From 08c56d0b9a8830a0f60bdf76eafbfe091128b2a7 Mon Sep 17 00:00:00 2001 From: itsumura-h Date: Sun, 23 Aug 2026 17:24:48 +0000 Subject: [PATCH 01/13] =?UTF-8?q?btree=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursor/rules/branch/118-icstablebtree.mdc | 641 ++++++++++++++++++ benchmarks/storage/stable_exact_lookup.nim | 74 ++ examples/stable_memory/backend/backend.did | 6 + examples/stable_memory/backend/src/main.nim | 68 +- .../src/stable_memory_backend/main.nim | 4 +- src/nicp_cdk/request.nim | 17 + src/nicp_cdk/storage/linear_hashing.nim | 36 + src/nicp_cdk/storage/memory_manager.nim | 152 +++++ src/nicp_cdk/storage/memory_view.nim | 84 +++ src/nicp_cdk/storage/stable_allocator.nim | 22 + src/nicp_cdk/storage/stable_btree.nim | 401 +++++++++++ src/nicp_cdk/storage/stable_hash.nim | 26 + src/nicp_cdk/storage/stable_hash_map.nim | 297 ++++++++ src/nicp_cdk/storage/stable_key_codec.nim | 98 +++ src/nicp_cdk/storage/stable_table.nim | 190 +----- .../storage/stable_table_migration.nim | 76 +++ tests/storage/test_linear_hashing.nim | 24 + tests/storage/test_memory_manager_api.nim | 17 + tests/storage/test_stable_btree.nim | 145 ++++ tests/storage/test_stable_btree_api.nim | 39 ++ tests/storage/test_stable_hash.nim | 14 + tests/storage/test_stable_hash_map.nim | 57 ++ tests/storage/test_stable_key_codec.nim | 29 + tests/types/test_nat.nim | 6 + 24 files changed, 2348 insertions(+), 175 deletions(-) create mode 100644 .cursor/rules/branch/118-icstablebtree.mdc create mode 100644 benchmarks/storage/stable_exact_lookup.nim create mode 100644 src/nicp_cdk/storage/linear_hashing.nim create mode 100644 src/nicp_cdk/storage/memory_manager.nim create mode 100644 src/nicp_cdk/storage/memory_view.nim create mode 100644 src/nicp_cdk/storage/stable_allocator.nim create mode 100644 src/nicp_cdk/storage/stable_btree.nim create mode 100644 src/nicp_cdk/storage/stable_hash.nim create mode 100644 src/nicp_cdk/storage/stable_hash_map.nim create mode 100644 src/nicp_cdk/storage/stable_key_codec.nim create mode 100644 src/nicp_cdk/storage/stable_table_migration.nim create mode 100644 tests/storage/test_linear_hashing.nim create mode 100644 tests/storage/test_memory_manager_api.nim create mode 100644 tests/storage/test_stable_btree.nim create mode 100644 tests/storage/test_stable_btree_api.nim create mode 100644 tests/storage/test_stable_hash.nim create mode 100644 tests/storage/test_stable_hash_map.nim create mode 100644 tests/storage/test_stable_key_codec.nim diff --git a/.cursor/rules/branch/118-icstablebtree.mdc b/.cursor/rules/branch/118-icstablebtree.mdc new file mode 100644 index 0000000..2b6c3bf --- /dev/null +++ b/.cursor/rules/branch/118-icstablebtree.mdc @@ -0,0 +1,641 @@ +# NICP Stable Memory Native Indexed Storage 設計書 + +**対象: github.com/dumblepy/nicp_cdk** + +Draft 0.1 \| 2026-08-20 + +| **設計結論** 第一実装は stable memory native の B+Tree とする。起動時はヘッダー・allocator メタデータのみを読み、全件走査や heap index 再構築を行わない。Hash index は exact-match 特化の第二実装とし、全件 rehash を避けるため linear hashing 等の incremental growth を採用する。 | +|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| **項目** | **決定** | +|-------------------|------------------------------------------------------------------| +| 主データ構造 | IcStableBTreeMap\[K,V\](B+Tree) | +| 既存 API | IcStableTable\[K,V\] を wrapper/alias として互換維持 | +| 起動コスト | O(1): header + allocator metadata + bounded cache initialization | +| heap 使用量 | O(cache + current result), データ件数に非依存 | +| 検索 | stable memory 上の node を逐次 read。O(log_B n) | +| range / iteration | linked leaf を順次走査。全件 heap 化なし | +| key ordering | 既存 serialize() と分離した order-preserving StableKeyCodec | +| 複数ストレージ | MemoryView 抽象化。将来 VirtualMemory/MemoryManager を標準化 | +| Hash | Phase 5。deterministic/versioned hash + incremental bucket split | + +# 1. 目的・スコープ + +## 進捗 + +- [x] Phase 1: `StableMemoryView`、永続 SBT2 superblock、順序保存 `StableKeyCodec`、固定ページ allocator の基盤を追加。 +- [x] Phase 2(MVP): stable memory 上の node/blob を直接辿る B+Tree の get/hasKey/insert/update/leaf・internal split/pairs を追加。 +- [x] Phase 2: value blob の persistent free-list allocator、bounded direct-mapped node cache、in-memory backend を用いる split/property/reopen test を追加。 +- [x] Phase 3: `lowerBound`、`range`、SBT2 を既定とする `IcStableTable` facade、bounded RawMemoryView の example を追加。`examples/stable_memory/backend` に `IcStableBTreeMap` の set/get/range scenario を追加。 +- [x] Phase 4(MVP): STBL v1 の永続カーソル付き `migrateStep` を追加。 +- [x] Phase 4: in-memory `MemoryView` backend により duplicate history・中断/再開 migration test を追加。 +- [x] Phase 4.5(MVP): persistent bucket chain の `MemoryManager` / `VirtualMemory` を追加。 +- [x] Phase 1: persisted codec ID を検証する明示的な custom `StableKeyCodec` 拡張点を追加。 +- [x] Phase 5(基盤): versioned SipHash-2-4 と persisted linear-hashing state transition を追加し、reference vector と split routing をテストした。 +- [x] Phase 5: `IcStableHashMap` の persistent bucket/overflow-chain 実装を追加。線形分割は1バケットだけを再配置し、全件 rehash を行わない。 +- [x] Phase 5: exact-match workload benchmark を `benchmarks/storage/stable_exact_lookup.nim` に追加。in-memory backend・cacheSlots=0 の結果では、1k entries は HashMap が優位(1 ms / 1.20 MB vs 4 ms / 3.12 MB)だが、10k entries は B+Tree が優位(54 ms / 41.62 MB vs 63 ms / 57.60 MB)だった。順序 API と大規模時の read byte を考慮し、`IcStableTable` の既定は B+Tree のままとする。 + +本設計の目的は、NICP の stable memory 上に「検索可能な index 自体」を永続化し、canister 起動・アップグレード後に全件を heap へ復元しなくても利用できる key-value storage を実装することである。対象は主に現行 IcStableTable の置き換えであり、既存の Nim らしい API は可能な限り維持する。 + +## 1.1 機能要件 + +- stable memory 上の index node/bucket を直接検索できること。 + +- 初期化時に全 key/value または全 key index を heap に読み込まないこと。 + +- canister upgrade 後の初期化コストが保存件数ではなく固定メタデータ量に依存すること。 + +- get / hasKey / set / len / clear / pairs / keys / values の既存 IcStableTable 相当 API を提供すること。 + +- 可変長 key/value を扱えること。value は既存 serialization を再利用可能とすること。 + +- 永続フォーマットを versioning し、互換性のない変更を検出できること。 + +- stable memory は shrink できない前提で、削除・更新で生じた空き領域を内部 allocator で再利用できること。 + +## 1.2 非機能要件 + +- heap footprint はデータ件数 O(n) ではなく bounded cache O(1) とする。 + +- stable read/write の回数と bytes を計測可能にする。 + +- trap 時の IC message atomicity を利用し、通常操作のための WAL を必須にしない。 + +- 既存 STBL v1 からの移行を、instruction limit を避ける stepwise migration として設計する。 + +- 将来の VirtualMemory、secondary index、certified index へ拡張可能な層構造とする。 + +## 1.3 非目標(初版) + +- SQL/query planner、複合 secondary index、MVCC を初版に含めない。 + +- float を B+Tree key として初版から一般サポートしない(NaN/total-order を明示設計するまでは除外)。 + +- 既存の全 stable storage を一度に置換しない。IcStableValue/IcStableSeq は独立して維持可能。 + +# 2. 現行 NICP 実装の分析 + +## 2.1 IcStableTable v1 の実体 + +現行 src/nicp_cdk/storage/stable_table.nim は、stable memory に append-only record log を保存し、heap 上の std/tables.Table\[string, EntryInfo\] を index として利用する。header は 32 bytes で、record は \[keyLen\]\[valueLen\]\[keyBytes\]\[valueBytes\] の連続形式である。 + +stable memory: +STBL header (32 B) +record 0: \[klen\]\[vlen\]\[key\]\[value\] +record 1: \[klen\]\[vlen\]\[key\]\[value\] +... + +heap: +Table\[string, EntryInfo\] \# serialized key -\> stable offset + +initIcStableTable() は readHeader() 後に常に rebuildIndex() を呼び、dataStart から dataEnd まで全 record を走査する。key は bytesToString() で heap string にコピーされ、同一 key の新しい record が古い EntryInfo を上書きする。したがって初期化コストは「live key 数」だけでなく更新履歴を含む historical record 数に比例する。 + +| **観点** | **現行 v1** | **問題** | +|-------------|-------------------------------------------|-----------------------------------------------------| +| 初期化 | 全 record scan + heap Table rebuild | O(history), upgrade/restart 後の命令数が増加 | +| heap | 全 unique key bytes + hash table metadata | O(n) heap。大規模化で不利 | +| lookup | heap hash -\> stable value read | 起動後は速いが index が非永続 | +| update | 常に末尾 append | 古い record が残り stable space が増える | +| iteration | heap index を列挙 | 全 key が heap にあることが前提 | +| multi-store | baseOffset を手動指定 | 成長上限を隔離しないため overlap を防ぐ仕組みがない | + +## 2.2 stable_memory.nim の利用上の注意 + +stable_memory.nim は stable64_size/grow/read/write を薄く wrap している。stableRead() は毎回 newSeq\[byte\] を作る一方、stableReadInto() は caller が用意した buffer に直接読む。B+Tree hot path では後者を優先し、node cache と固定サイズ buffer を併用して一時 heap allocation を抑える。 + +## 2.3 serialization.nim と key ordering の不一致 + +現行 serialize() は storage encoding として little-endian を使い、string/Principal には 4-byte length prefix を付ける。この byte sequence は B-tree の sort key として一般には利用できない。例えば uint16 の 255 は FF 00、256 は 00 01 となるため byte lexicographic order は数値順と逆転する。string も length prefix が内容より先に比較される。 + +| **設計上の分離** ValueCodec(保存・復元)と StableKeyCodec(全順序を保つ検索キー)を別インターフェースにする。既存 serialize() は value には再利用できるが、B+Tree key comparator の仕様にはしない。 | +|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +# 3. 既存実装の調査 + +## 3.1 Rust: DFINITY ic-stable-structures + +ICP 公式 Rust ドキュメントは stable structures を「heap を迂回して stable memory を直接 read/write する構造」と位置付け、large state で pre_upgrade/post_upgrade serialization を不要にする方式として推奨している。ic-stable-structures 0.7 系の StableBTreeMap は persistent header に root address と length を持ち、allocator と B-tree node も stable memory に置く。 + +source の BTreeMap::init/load は全 node を復元せず、header と allocator metadata を読み込む。node は address で必要時に load される。現在の実装は direct-mapped node cache を持ち、default 16 slots、0 で無効化できる。V2 node は key/value の lazy load も行う。 + +| **要素** | **Rust stable-structures の方式** | **NICP への示唆** | +|---------------|----------------------------------------------------|----------------------------------------| +| Header | magic/version/root/length + reserved | 起動時 O(1) metadata load | +| Allocator | persistent free-list chunk allocator | free list 自体を stable memory に置く | +| Node access | address 指定で stable read、必要時のみ deserialize | 全 tree を heap 化しない | +| Node cache | bounded direct-mapped cache | 16 slots 程度から benchmark | +| MemoryManager | 最大 255 virtual memory、bucket 単位で成長 | baseOffset 手動管理を抽象化 | +| Format | layout version + reserved bytes | 永続 schema を frozen/versioned にする | + +## 3.2 Rust: ic-stable-memory SHashMap + +community crate ic-stable-memory の SHashMap は stable memory 上の open addressing / linear probing hash table である。hash は upgrade 間で deterministic にするため固定アルゴリズムを使い、occupancy byte + fixed-size K/V slot を stable block に保存する。heap 側には table pointer、len、capacity 等の小さな metadata だけを持つ。 + +一方、load factor 約 75% で容量を広げる際に新 table を確保し、既存 key を全件 rehash する。この「一回の resize が O(n)」という性質は、IC の instruction budget と相性が悪い。また K/V が fixed-size 前提であり、NICP の string/Principal/object value には pointer/blob allocator を追加する必要がある。 + +## 3.3 Motoko: 公式 persistence と direct stable-memory structure の区別 + +現行 Motoko は enhanced orthogonal persistence が default で、Wasm main memory(stable heap)を upgrade 後も保持し、heap size に依存しない upgrade を実現する。したがって Motoko の StableHashMap/StableRBTree という名称は Rust の stable structures と同義ではなく、公式ドキュメントも「stable type であり direct stable-memory structure ではない」と明記している。 + +ただし Motoko でも raw stable memory / Region は利用できる。Region は explicit layout が必要で access cost もあるため、公式には enhanced orthogonal persistence が合わない場合に限定して使うことを勧めている。NICP は Nim/C/Wasm のため Motoko の heap retention をそのまま利用できず、Rust 型の explicit stable structure が本設計の直接的な比較対象になる。 + +## 3.4 Motoko community: NatLabs MemoryBTree + +NatLabs/memory-collection の MemoryBTree は、branch node、leaf node、serialized key、value の全てを stable memory に置く B+Tree である。branch/leaves/key/value を 4 つの MemoryRegion に分け、leaf を linked list 化し、key comparator は serialized Blob を直接比較できるように設計されている。repository は現在 archived だが、layout と比較戦略は NICP に有用な prior art である。 + +| **比較軸** | **DFINITY BTreeMap** | **NatLabs MemoryBTree** | **NICP 提案** | +|------------------|-----------------------------|----------------------------|----------------------------------| +| tree | B-tree | B+Tree | B+Tree | +| values | node 内 lazy value/overflow | value region 分離 | value blob 分離 | +| iteration | tree iterator | linked leaf | linked leaf | +| key compare | Storable + Ord | serialized Blob comparator | StableKeyCodec / encoded compare | +| cache | bounded node cache | stable memory + cache可能 | bounded cache 0/1/16/32 | +| memory isolation | MemoryManager | MemoryRegion | MemoryView → VirtualMemory | + +# 4. Architecture Decision: B+Tree を第一実装にする + +| **評価** | **B+Tree** | **Open-address Hash** | **Linear Hashing** | +|--------------------|--------------------|-----------------------|-----------------------| +| exact lookup | O(log_B n) | 平均 O(1) | 平均 O(1) | +| range/order | 強い | 不可 | 不可 | +| iteration | leaf sequential | bucket scan | bucket scan | +| growth spike | node split のみ | full rehash O(n) | 1 bucket split に分散 | +| variable key/value | pointer 化で自然 | slot + pointer 必要 | bucket + pointer 必要 | +| collision attack | なし | hash 依存 | hash 依存 | +| 実装参考 | 公式 Rust + Motoko | Rust community | 自前設計が中心 | +| 初版適性 | 高 | 中〜低 | 中 | + +IcStableTable の既存 API は exact lookup だけでなく pairs/keys/values を持つ。B+Tree はこれらを自然に実装でき、将来 range/lowerBound を追加できる。さらに node split は局所操作で、hash table resize のような全件 rehash がない。したがって初版は DFINITY 型の B-tree そのものではなく、data を leaf に集約し leaf 同士を link した B+Tree を採用する。本文では B-tree と B+Tree を区別して記述する。 + +| **ADR-001** 主実装は B+Tree(内部 node は separator、data は leaf にのみ保持)とする。DFINITY の B-tree より iteration/range を単純化し、NatLabs の「node と key/value blob 分離」を取り入れる。Hash は別型 IcStableHashMap として後置する。 | +|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +# 5. 提案アーキテクチャ + +## 5.1 レイヤ構成 + +Application / canister code +\| +v +IcStableTable\[K,V\] (compatibility wrapper) +\| +v +IcStableBTreeMap\[K,V\] (search/insert/split/iteration) +\| \| \| +\| \| +-- NodeCache (bounded heap) +\| +------------- StableKeyCodec\[K\] / ValueCodec\[V\] ++------------------------ NodeStore / BlobStore / StableAllocator +\| +v +StableMemoryView +/ \\ +RawMemoryView VirtualMemory (future/default) +\| +v +ic0_stable64\_\* / stable memory + +## 5.2 各コンポーネント + +| **コンポーネント** | **責務** | **heap 常駐** | +|---------------------|-------------------------------------------------------------------------------------|-----------------------| +| StableMemoryView | size/grow/read/write の論理 address space。base offset/limit/virtual mapping を隠蔽 | 小さな handle | +| StableAllocator | fixed node page と variable blob の allocate/free。free list/bin head を永続化 | header metadata のみ | +| StableKeyCodec\[K\] | 検索順を保つ encode/compare、codec ID/version | query key buffer のみ | +| NodeStore | node header/slot の decode/encode、stable read/write | current node buffer | +| NodeCache | hot node の bounded cache | 設定 slot 数のみ | +| IcStableBTreeMap | root/height/count、探索・split・iterator | header + cache | +| IcStableTable | 既存 API の facade | ほぼなし | + +## 5.3 起動シーケンス + +1\. StableMemoryView を open する。 + +2\. B+Tree header の magic/version/nodeSize/codecId を読む。 + +3\. allocator header(node free head、blob free bins、arena end)を読む。 + +4\. root address、height、count、firstLeaf/lastLeaf を heap の小さな struct に保持する。 + +5\. NodeCache を指定 slot 数だけ初期化する。root を先読みする場合でも 1 node のみ。 + +6\. record scan は一切行わない。 + +| **目標** init は保存件数 0 / 1,000 / 1,000,000 で stable read bytes と heap allocation がほぼ同じになることを acceptance test にする。 | +|----------------------------------------------------------------------------------------------------------------------------------------| + +# 6. 永続メモリレイアウト案 + +## 6.1 B+Tree Superblock + +初版は 256 bytes を予約する。field を packed little-endian で固定し、未使用領域を zero/reserved とする。header 自体は sort order に関係しないため little-endian でよい。 + +| **Offset** | **Size** | **Field** | **説明** | +|------------|----------|----------------------------------------------|-------------------------------| +| 0 | 4 | magic = "SBT2" | type 識別 | +| 4 | 2 | layoutVersion | node/header layout version | +| 6 | 2 | flags | feature flags | +| 8 | 4 | nodeSize | 1024 default; persisted | +| 12 | 4 | keyCodecId | 順序 encoding の互換性 ID | +| 16 | 4 | valueCodecId | value serialization schema ID | +| 20 | 4 | reserved | alignment/future | +| 24 | 8 | count | live entries | +| 32 | 8 | rootAddr | 0 = empty | +| 40 | 4 | height | root leaf = 1 | +| 44 | 4 | reserved | alignment | +| 48 | 8 | firstLeaf | iteration start | +| 56 | 8 | lastLeaf | reverse/future | +| 64 | 8 | nodeFreeHead | fixed-page allocator | +| 72 | 8 | nodeArenaEnd | next node allocation | +| 80 | 8 | keyArenaEnd | key blob allocator | +| 88 | 8 | valueArenaEnd | value blob allocator | +| 96 | ... | free bins / generation / checksum / reserved | future compatibility | + +## 6.2 Node page + +nodeSize は 1024 bytes を default とし、1 KiB / 2 KiB / 4 KiB を benchmark して最終決定する。node header と slot array は固定長にする。variable key/value bytes は node page へ埋め込まず別 arena に置くことで split/merge のコピー量を抑える。 + +| **Node type** | **Header** | **Slot(例)** | **特徴** | +|---------------|-------------------------------------------|----------------------------------------------------------------------|-----------------------------------------------| +| Internal | type,keyCount,parent,leftChild,generation | keyOff:u64, keyLen:u32, flags:u32, rightChild:u64 (24 B) | separator key + child pointer | +| Leaf | type,keyCount,parent,prev,next,generation | keyOff:u64,keyLen:u32,valueOff:u64,valueLen:u32,keyPrefix:u64 (32 B) | linked leaf、8-byte prefix は比較 accelerator | + +1 KiB node の場合、48〜64 B header を除けば leaf は約 30 slots、internal は約 40 slots を保持できる。実 occupancy を 50〜75% としても fanout は十分大きく、百万件級で tree height は概ね数段に収まる。最終値は benchmark で決め、nodeSize は header に永続化して deployment 後に無断変更しない。 + +## 6.3 Key / Value blob block + +BlobBlockHeader (16 bytes example) +blockSize : u32 \# header included / aligned +payloadSize : u32 +flags : u32 \# allocated/free/type +classId : u16 +reserved : u16 +\[payload bytes ...\] + +free block when released: +payload head can store nextFree:u64 + +variable-size allocator は segregated free-list(size class)を推奨する。例えば 32,64,128,... bytes の bin head を superblock/allocator header に永続化し、更新時に旧 value block を free して再利用する。巨大 block は dedicated list とする。初版の実装量を抑える場合でも「append-only blob を永続仕様に固定」せず allocator interface を先に切り、後から回収方式を差し替えられるようにする。 + +## 6.4 MemoryView と VirtualMemory + +現行 baseOffset は「開始位置」を指定するだけで、構造 A が成長して構造 B の baseOffset に到達することを防がない。B+Tree 自体は StableMemoryView の論理 address だけを見るようにし、backend を切り替えられるようにする。 + +| **Mode** | **用途** | **制約** | +|----------------------------|-------------------------------------|------------------------------------------------------------------| +| RawMemoryView(base, limit) | 既存 API 互換・テスト・単一専用領域 | limit 必須を推奨。複数構造の手動 layout は上級者向け | +| VirtualMemory(memoryId) | 一般利用の default 目標 | bucket/page mapping を manager が管理し、各 structure は独立成長 | + +Rust MemoryManager は first page に manager state を置き、virtual memory を bucket list として表現する。NICP も同型の bucket mapping を実装できるが、既存 STBL が offset 0 を利用している canister では manager header を 0 に新設できない。このため managerBase を指定可能にするか、migration 専用 transitional version で安全な位置を確定してから導入する。 + +# 7. StableKeyCodec 設計 + +## 7.1 原則 + +- 同一 codec ID では、encode(a) の byte lexicographic order と論理比較 a \< b が一致すること。 + +- codec ID/version は superblock に保存し、open 時に compiled codec と一致しなければ error にする。 + +- value serialization の version と key ordering version を分離する。 + +- custom object key は暗黙の fieldPairs serialization に頼らず、明示 codec を要求する。 + +## 7.2 Built-in key codec 案 + +| **型** | **ordered encoding** | **備考** | +|----------------|----------------------------------------------------------------------|----------------------------------------------| +| uint8/16/32/64 | fixed width big-endian | byte lex = numeric order | +| int8/16/32/64 | two’s complement bit pattern の sign bit を flip → big-endian | negative \< positive を維持 | +| bool | 00 / 01 | false \< true | +| char | code unit を unsigned ordered encoding | Nim char の定義範囲を固定 | +| string | length prefix を比較キーから除外し UTF-8/raw bytes lex | storage block length は slot metadata に保持 | +| Principal | raw principal bytes の lexicographic order を仕様化 | length は metadata | +| int/uint | 実 width を codec ID に含める。可能なら explicit int64/uint64 を推奨 | target/ABI 差を防ぐ | +| float32/64 | Phase 1 unsupported | NaN を含む total-order を別 ADR で定義 | +| object/tuple | user-defined StableKeyCodec | composite index へ拡張可能 | + +## 7.3 比較時の allocation を抑える + +query key は method entry で一度 ordered bytes に encode する。node slot に keyPrefix(先頭 8 bytes または fingerprint)を持たせ、prefix で順序が決まらない場合だけ key blob を stableReadInto() で読む。stored key を K に deserialize して comparator を呼ぶ方式は fallback とし、hot path では encoded bytes のまま比較する。 + +# 8. 基本アルゴリズム + +## 8.1 get / hasKey + +query = StableKeyCodec.encode(key) +addr = header.rootAddr +while addr != 0: +node = cache.getOrRead(addr) +if node.isLeaf: +i = lowerBound(node.slots, query, compareStoredKey) +if i matches: +return readValue(node.slots\[i\].valueOff, valueLen) +return notFound +else: +child = chooseChildBySeparators(node, query) +addr = child + +heap に保持するのは query key、現在 node buffer、cache slots、返却する value のみである。全件 index は存在しない。hasKey は value blob を読まず leaf match までで終了する。 + +## 8.2 insert / update + +1\. root から leaf まで search path を辿る。必要なら parent address を node に持つか、path の node address だけを小さな stack に保持する。 + +2\. 既存 key の場合、新 value block を allocate/write し、leaf slot の value pointer を差し替える。成功後に旧 block を free list へ返す。 + +3\. 新規 key の場合、key/value block を allocate し leaf slot を挿入する。 + +4\. leaf が overflow した場合は leaf split。右 leaf を allocate、entries を分割、prev/next link を更新し separator を parent に挿入する。 + +5\. parent overflow は internal split を root まで伝播する。root split のときだけ height と rootAddr を更新する。 + +6\. 最後に count/header metadata を更新する。IC message が成功した場合のみ stable changes が commit され、trap なら message の変更は commit されない。 + +## 8.3 iteration / range + +pairs()/keys()/values() は firstLeaf から next pointer を辿る。各 leaf を 1 page ずつ読み、その page の slot を順に yield する。range(start,end) は start key を通常 search して最初の leaf/slot を求め、その後 linked leaf を順次辿る。iterator が全 key を heap に展開しないことを保証する。 + +## 8.4 clear + +structure が専用 MemoryView を所有する場合、clear は新しい empty root と allocator state へ reset することで論理 O(1) にできる。underlying stable memory 自体は shrink しないが、同じ view 内の既存 node/blob 領域を再利用できるよう allocator generation/arena reset を行う。VirtualMemory の bucket が underlying memory manager に返却されるかは別レイヤの policy とし、初版では返却不要でもよい。 + +## 8.5 remove(将来/optional) + +現行 IcStableTable API には per-key delete がないため、B+Tree MVP では remove を必須にしない。追加する場合は leaf slot 削除 + blob free を先に実装し、underflow rebalance/merge は Phase 2 でもよい。rebalance を遅延する場合、検索の正しさと minimum occupancy の緩和を format flag に明示する。 + +# 9. Optional IcStableHashMap 設計 + +## 9.1 なぜ従来 open addressing をそのまま採らないか + +stable memory 上の open addressing 自体は成立するが、capacity growth で table 全体を rehash すると、ある 1 update message が O(n) になる。IC の instruction budget を考えると、large state 向け storage の成長操作に全件処理を埋め込むのは避けるべきである。 + +## 9.2 Linear Hashing 案 + +Hash 版を実装する場合は linear hashing を推奨する。load factor 閾値を超えるたびに全 table ではなく 1 bucket だけ split し、growth cost を多数の update に分散する。 + +persistent header: +magic/version/hashAlgorithmId/hashSeed +count, level, split, bucketCount, bucketCapacity + +bucket(key): +h = stableHash(seed, encodedKey) +b = h & ((1 \<\< level) - 1) +if b \< split: +b = h & ((1 \<\< (level + 1)) - 1) + +when grow: +split bucket\[split\] only +split += 1 +if split == (1 \<\< level): +level += 1 +split = 0 + +| **設計点** | **要求** | +|------------|------------------------------------------------------------------------------------------------| +| hash | algorithm と seed を header に保存。upgrade で結果が変わらない | +| security | 外部入力 key を想定し collision flooding を考慮。単に language default hash を永続仕様にしない | +| bucket | fixed-size page + slot directory + overflow chain | +| slot | fingerprint + keyRef/keyLen + valueRef/valueLen | +| growth | 1 bucket incremental split。full rehash 禁止 | +| iteration | bucket order。順序 API は提供しない | + +| **優先順位** IcStableHashMap は B+Tree の後に実装する。exact-match が支配的な workload で benchmark 上明確な差が出る場合に採用し、IcStableTable の default backend にはしない。 | +|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +# 10. Upgrade / Migration 設計 + +## 10.1 v2 format の upgrade safety + +- magic + layoutVersion + nodeSize + keyCodecId + valueCodecId を open 時に検証する。 + +- unsupported layout を「新規 empty tree」と誤認しない。必ず explicit error/trap で止める。 + +- header に reserved bytes を十分確保し、field 追加で既存 offset を動かさない。 + +- codec の変更は in-place reinterpret せず migration を要求する。 + +- object value schema の field order 変更等は既存 serialization 互換性を破るため、valueCodecId を application が管理できるようにする。 + +## 10.2 STBL v1 からの移行 + +現行 v1 は append-only で同一 key の更新を新 record として末尾へ追加する。したがって migration を offset 昇順に upsert すれば、最後の record が最終値になる。ただし 1 message で全 record を B+Tree へ入れると instruction limit に達し得るため、incremental migration を行う。 + +| **Stage** | **動作** | +|-------------------------|---------------------------------------------------------------------------------------------------------------------------------| +| A. Transitional release | 従来 IcStableTable をそのまま open(この release では最後の full heap rebuild を許容)。v2 store 用の安全な MemoryView を作成。 | +| B. Freeze / maintenance | v1 への write を止める。read-only service は heap index で継続可能。 | +| C. migrateStep(N) | persisted migrationCursor から最大 N records / bytes を順次読み、v2 B+Tree へ upsert。cursor は同一 message で更新。 | +| D. Verify | v1 unique count と v2 count、sample/hash verification、全 migration 完了 flag を確認。 | +| E. Native release | IcStableTable を v2 wrapper に切替。以後起動時 full scan は消える。 | + +online dual-write migration も可能だが、migration 中に古い v1 record が新しい dual-write value を上書きしないため source offset/version の比較が必要になる。初版は maintenance-mode incremental migration を推奨し、online migration は別 ADR とする。 + +## 10.3 IC message atomicity と WAL + +ICP 公式ドキュメントでは、Wasm/stable memory の変更は message が成功したときに commit され、message execution が失敗した場合は commit されない。したがって node split の途中で trap したケースを回復するための一般 WAL は必須ではない。ただし論理バグ・format corruption 検出のため generation、magic、bounds check、debug verify を持たせる。 + +# 11. Public API 案 + +## 11.1 新型 + +type IcStableBTreeMap\*\[K, V\] = object +\# persistent data is NOT stored here +memory: StableMemoryView +header: BTreeHeader +cache: NodeCache + +proc initIcStableBTreeMap\*\[K, V\]( +memory: StableMemoryView, +cacheSlots: int = 16 +): IcStableBTreeMap\[K, V\] + +proc hasKey\*\[K,V\](t: IcStableBTreeMap\[K,V\], key: K): bool +proc \`\[\]\`\*\[K,V\](t: var IcStableBTreeMap\[K,V\], key: K): V +proc \`\[\]=\`\*\[K,V\](t: var IcStableBTreeMap\[K,V\], key: K, value: V) +proc len\*\[K,V\](t: IcStableBTreeMap\[K,V\]): int +proc clear\*\[K,V\](t: var IcStableBTreeMap\[K,V\]) +iterator pairs\*\[K,V\](t: var IcStableBTreeMap\[K,V\]): (K,V) +iterator keys\*\[K,V\](t: var IcStableBTreeMap\[K,V\]): K +iterator values\*\[K,V\](t: var IcStableBTreeMap\[K,V\]): V + +\# new ordered APIs +iterator range\*\[K,V\](t: var IcStableBTreeMap\[K,V\], startKey, endKey: K): (K,V) +proc lowerBound\*\[K,V\](t: var IcStableBTreeMap\[K,V\], key: K): Option\[(K,V)\] + +## 11.2 IcStableTable compatibility + +既存 user code の変更を抑えるため、stable_table.nim は facade として残す。fresh deployment では v2 backend を default にし、旧 format は stable_table_v1.nim へ固定する。既存 canister の自動判定で silent migration は行わず、STBL magic を検出した場合は migration-required を明示する。 + +\# target behavior +var users = initIcStableTable\[string, User\](memoryId = 10) +users\["alice"\] = user +let u = users\["alice"\] +for k, v in users.pairs(): +discard + +\# internal: IcStableTable delegates to IcStableBTreeMap + +## 11.3 Memory API の段階導入 + +現行 initIcStableTable(baseOffset=0) を直ちに削除せず、RawMemoryView(baseOffset, limit) を経由する deprecated compatibility overload を用意する。一方、新 API は memoryId または StableMemoryView を受け取る形を標準にする。baseOffset だけで無制限 grow する API は新規利用を非推奨にする。 + +# 12. 実装モジュール構成案 + +| **File** | **責務** | +|-------------------------------------------------|--------------------------------------------------| +| src/nicp_cdk/storage/stable_memory.nim | 既存 ic0 wrapper。readInto primitive を拡充 | +| src/nicp_cdk/storage/memory_view.nim | StableMemoryView interface + RawMemoryView | +| src/nicp_cdk/storage/memory_manager.nim | VirtualMemory / bucket mapping(段階導入) | +| src/nicp_cdk/storage/stable_allocator.nim | fixed page allocator + blob free bins | +| src/nicp_cdk/storage/stable_key_codec.nim | built-in ordered codecs + custom extension point | +| src/nicp_cdk/storage/stable_btree_node.nim | node layout, read/write, binary search | +| src/nicp_cdk/storage/stable_btree.nim | B+Tree algorithms / iterator / cache | +| src/nicp_cdk/storage/stable_table_v1.nim | legacy STBL implementation frozen | +| src/nicp_cdk/storage/stable_table.nim | v2 facade / compatibility | +| src/nicp_cdk/storage/stable_table_migration.nim | STBL → SBT2 stepwise migration | +| tests/storage/test_stable_btree.nim | unit/property/reopen tests | +| examples/stable_memory_indexed/... | upgrade + large-data example | + +# 13. Test / Benchmark 計画 + +## 13.1 Correctness + +- random insert/update/get を Nim std Table + sorted reference と比較する property test。 + +- node split: leftmost/rightmost/middle、root split、連続 split。 + +- variable key/value: empty string、長い string、Principal、large object value。 + +- reopen: object を破棄→同じ stable bytes から init→全 lookup が一致。init で record scan されないことを instrumentation で確認。 + +- iteration/range が stable key order で全件 1 回ずつ返す。 + +- magic/version/nodeSize/codec mismatch を reject。corrupt offset/length を bounds check で reject。 + +- trap injection: split/write の各段階で意図的 trap → message rollback 後に tree が旧状態のまま。 + +- legacy migration: duplicate key history、empty table、中断/resume、複数 migrateStep。 + +## 13.2 Performance + +| **Metric** | **v1 現行** | **v2 target** | +|-------------------|--------------------------------|-----------------------------------------------------------------| +| init stable reads | 全 historical records | 固定: superblock + allocator metadata(root prefetch optional) | +| init heap | O(unique keys) | O(cache slots) | +| get | heap hash + value read | O(tree height) node reads + value read | +| set existing | append | search + value replace/free | +| iteration | heap key index + stable values | leaf sequential read | +| growth spike | init/rebuild が最大 | node split の局所コスト | + +benchmark dataset は 1k / 10k / 100k / 1M live keys、update history 1x / 10x を用意する。特に v1 は 10x update history で init cost が増えるため、v2 の「history 非依存」を明確に測る。nodeSize 1KiB/2KiB/4KiB、cacheSlots 0/1/16/32 を比較する。 + +| **Acceptance target** | **基準** | +|-----------------------|--------------------------------------------------------------------------------------| +| Startup | 1M keys でも全件 scan なし。entry count に比例する stableRead 呼び出しが発生しない | +| Heap | default cache を除き key count に比例する persistent index object を heap に持たない | +| Lookup | search path 以外の node/record を読まない | +| Upgrade | v2→同一 codec/layout upgrade で migration 不要 | +| Memory safety | 全 stable address/length を view bounds 内で検証 | +| Compatibility | 既存 public IcStableTable 基本 API の compile/use pattern を維持 | + +# 14. リスクと対策 + +| **Risk** | **影響** | **対策** | +|---------------------------------|----------------------------------------|-------------------------------------------------------------------------| +| stable read の instruction cost | heap hash より lookup が遅くなる可能性 | high fanout + binary search + prefix + bounded cache + benchmark | +| key codec 仕様変更 | tree ordering が破壊 | codecId/version を persisted、in-place change 禁止 | +| value schema 変更 | deserialize failure/意味破壊 | valueCodecId + explicit migration policy | +| variable blob fragmentation | stable memory 使用量増加 | segregated free bins、key/value arena 分離、metrics | +| baseOffset collision | データ破損 | MemoryView limit + VirtualMemory を標準化 | +| hash flooding | DoS/instruction spike | Hash 版は deterministic seeded hash + fingerprint + bucket chain limits | +| memory manager bucket leak | clear 後も physical allocation 残存 | 初版は既知制約として明示。将来 reclamation | +| migration instruction limit | upgrade不能 | transitional release + bounded migrateStep | +| Nim generics/ABI width | persistent key encoding 差異 | fixed-width codec を優先、int/uint width を codec ID に含める | + +# 15. 実装フェーズ + +| **Phase** | **Deliverable** | **Exit criteria** | +|-------------------|----------------------------------------------------------------|----------------------------------------------------| +| 0\. Measurement | v1 init/read/write instrumentation + baseline benchmark | history 1x/10x の init cost を再現 | +| 1\. Foundation | MemoryView, BTree header, fixed node allocator, StableKeyCodec | reopen で header/allocator を O(1) load | +| 2\. B+Tree core | get/hasKey/insert/update/split + node cache | random property tests pass、1M synthetic build | +| 3\. API | iteration/range/clear、IcStableTable facade、docs/example | 既存 stable table sample を v2 で動作 | +| 4\. Migration | stable_table_v1 freeze + incremental migration tool | 中断/resume + duplicate history test pass | +| 4.5 MemoryManager | VirtualMemory bucket mapping を一般 API に昇格 | 複数 growing structures が isolation | +| 5\. Hash optional | IcStableHashMap with linear hashing | full rehash なし、exact-match benchmark で採用判断 | + +Phase 4.5 は技術的には Phase 1 前に実装してもよい。既存 deployed canister との offset compatibility が最も難しいため、B+Tree core と memory abstraction を先に完成させ、manager は separate module として導入する順序を推奨する。fresh project では VirtualMemory を default にする。 + +# 16. 実装時の具体的判断 + +- 「stable index を heap に mirror する」設計は採用しない。cache は bounded で、全件 rebuild code path を v2 に持ち込まない。 + +- B+Tree node は variable-length payload を直接詰め込み過ぎず、slot は pointer/reference 中心にする。split cost と format complexity を抑える。 + +- 既存 serialize(key) を byte comparator に流用しない。ordered key codec を独立させる。 + +- stableRead() の newSeq allocation を hot path で乱用せず stableReadInto() と reusable buffers を使う。 + +- header/node の every offset/length を stable memory size と MemoryView limit に対して検証する。corruption で任意 read を起こさない。 + +- node cache は性能 optimization であり correctness dependency にしない。cacheSlots=0 でも全 test が通ること。 + +- format version と codec version をテスト fixture として repository に固定し、将来 refactor でも bytes compatibility を検証する。 + +- Hash を追加するときは Rust SHashMap の「deterministic hash」は学ぶが、「75% で full rehash」は踏襲しない。 + +# 17. 最終提案 + +NICP の stable storage v2 は「stable memory を保存場所として使う」のではなく、「stable memory 自体を searchable address space として使う」設計へ変更する。現行 v1 の append-only data + heap index は小規模では単純だが、起動時 scan と heap O(n) が large state の上限になる。 + +最初に IcStableBTreeMap\[K,V\] を B+Tree として実装し、root/allocator/node/key/value を stable memory に永続化する。heap には superblock metadata と小さな node cache だけを置く。IcStableTable はこの backend の wrapper として API 互換を維持する。key の ordered codec は existing serialization と分離する。 + +Hash index は別用途として有効だが、単純 open addressing の full rehash は避ける。必要になった時点で linear hashing の incremental bucket split を実装し、exact-match workload の benchmark で B+Tree より優位なケースに限定して選択できるようにする。 + +| **Go / No-Go** Go: B+Tree v2。No-Go: 現行 append log に persistent hash table の全 key を別途 mirror するだけの設計。後者は index 再構築問題を消しても、二重 storage・rehash・migration complexity を増やしやすい。 | +|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +# 参考資料 + +- **[R1] NICP current StableTable** + https://github.com/dumblepy/nicp_cdk/blob/main/src/nicp_cdk/storage/stable_table.nim + +- **[R2] NICP stable memory wrapper** + https://github.com/dumblepy/nicp_cdk/blob/main/src/nicp_cdk/storage/stable_memory.nim + +- **[R3] NICP serialization** + https://github.com/dumblepy/nicp_cdk/blob/main/src/nicp_cdk/storage/serialization.nim + +- **[R4] NICP stable memory documentation** + https://github.com/dumblepy/nicp_cdk/blob/main/docs/en/stable_memory.md + +- **[R5] ICP Developer Docs - Stable structures (Rust)** + https://docs.internetcomputer.org/languages/rust/stable-structures/ + +- **[R6] ic-stable-structures BTreeMap 0.7.2 docs** + https://docs.rs/ic-stable-structures/latest/ic_stable_structures/btreemap/index.html + +- **[R7] ic-stable-structures MemoryManager 0.7.2 docs** + https://docs.rs/ic-stable-structures/latest/ic_stable_structures/memory_manager/struct.MemoryManager.html + +- **[R8] DFINITY stable-structures btreemap source** + https://github.com/dfinity/stable-structures/blob/main/src/btreemap.rs + +- **[R9] DFINITY stable-structures allocator source** + https://github.com/dfinity/stable-structures/blob/main/src/btreemap/allocator.rs + +- **[R10] DFINITY stable-structures node source** + https://github.com/dfinity/stable-structures/blob/main/src/btreemap/node.rs + +- **[R11] ic-stable-memory SHashMap source** + https://github.com/seniorjoinu/ic-stable-memory/blob/master/src/collections/hash_map/mod.rs + +- **[R12] ICP Developer Docs - Enhanced orthogonal persistence (Motoko)** + https://docs.internetcomputer.org/languages/motoko/fundamentals/actors/orthogonal-persistence/enhanced/ + +- **[R13] ICP Developer Docs - Stable memory and regions (Motoko)** + https://docs.internetcomputer.org/languages/motoko/icp-features/stable-memory/ + +- **[R14] NatLabs MemoryBTree README (archived repository)** + https://github.com/NatLabs/memory-collection/blob/main/src/MemoryBTree/readme.md + +調査時点: 2026-08-20。GitHub の対象 repository は main/master の公開 source を参照。Motoko community の NatLabs/memory-collection は archived であるため、推奨ライブラリというより設計 prior art として扱った。 diff --git a/benchmarks/storage/stable_exact_lookup.nim b/benchmarks/storage/stable_exact_lookup.nim new file mode 100644 index 0000000..b9a1e71 --- /dev/null +++ b/benchmarks/storage/stable_exact_lookup.nim @@ -0,0 +1,74 @@ +## Exact-match lookup comparison for the stable-memory-native indices. +## +## Run with: +## nim c -d:release -d:nicpMemoryViewOnly -r --skipUserCfg benchmarks/storage/stable_exact_lookup.nim +## nim c -d:release -d:nicpMemoryViewOnly -r --skipUserCfg benchmarks/storage/stable_exact_lookup.nim 100000 +## +## Wall-clock values are environment-dependent. The read-call and read-byte +## counters are the portable result: they model the stable-memory work that a +## canister has to perform for the workload. + +import std/[monotimes, strformat, os, times, strutils] +import ../../src/nicp_cdk/storage/memory_view +import ../../src/nicp_cdk/storage/stable_btree +import ../../src/nicp_cdk/storage/stable_hash_map + +type InMemoryStable = ref object + data: seq[byte] + readCalls, readBytes: uint64 + +proc memoryView(memory: InMemoryStable): StableMemoryView = + initMemoryView( + proc(): uint64 = uint64(memory.data.len), + proc(offset, size: uint64): seq[byte] = + if offset > uint64(memory.data.len) or size > uint64(memory.data.len) - offset: + raise newException(ValueError, "benchmark memory read out of bounds") + inc memory.readCalls + memory.readBytes += size + memory.data[int(offset) ..< int(offset + size)], + proc(offset: uint64, data: seq[byte]) = + let endOffset = int(offset) + data.len + if endOffset > memory.data.len: memory.data.setLen(endOffset) + for i, value in data: memory.data[int(offset) + i] = value + ) + +proc resetReads(memory: InMemoryStable) = + memory.readCalls = 0 + memory.readBytes = 0 + +proc queryOrder(index, count: uint32): uint32 = + ## Deterministic, non-sequential key distribution for lookup workloads. + uint32((uint64(index) * 2_654_435_761'u64) mod uint64(count)) + +proc benchmark(count: uint32) = + let treeMemory = InMemoryStable(data: @[]) + let hashMemory = InMemoryStable(data: @[]) + var tree = initIcStableBTreeMap[uint32, uint64](treeMemory.memoryView(), cacheSlots = 0) + var hash = initIcStableHashMap[uint32, uint64](hashMemory.memoryView()) + for key in 0'u32 ..< count: + let value = uint64(key) xor 0x9e3779b97f4a7c15'u64 + tree[key] = value + hash[key] = value + + treeMemory.resetReads() + var treeChecksum = 0'u64 + let treeStart = getMonoTime() + for index in 0'u32 ..< count: + treeChecksum = treeChecksum xor tree[queryOrder(index, count)] + let treeElapsed = getMonoTime() - treeStart + + hashMemory.resetReads() + var hashChecksum = 0'u64 + let hashStart = getMonoTime() + for index in 0'u32 ..< count: + hashChecksum = hashChecksum xor hash[queryOrder(index, count)] + let hashElapsed = getMonoTime() - hashStart + doAssert treeChecksum == hashChecksum + + echo &"entries={count}" + echo &" btree: {inMilliseconds(treeElapsed)} ms, reads={treeMemory.readCalls}, bytes={treeMemory.readBytes}" + echo &" hash: {inMilliseconds(hashElapsed)} ms, reads={hashMemory.readCalls}, bytes={hashMemory.readBytes}" + +let count = if paramCount() == 1: parseUInt(paramStr(1)).uint32 else: 10_000'u32 +if count == 0: raise newException(ValueError, "entry count must be positive") +benchmark(count) diff --git a/examples/stable_memory/backend/backend.did b/examples/stable_memory/backend/backend.did index 5806db8..1d33b99 100644 --- a/examples/stable_memory/backend/backend.did +++ b/examples/stable_memory/backend/backend.did @@ -35,4 +35,10 @@ service : { "table_values": () -> (vec text) query; "object_set": (id : nat, name : text, active : bool) -> (); "object_get": () -> (record { id : nat; name : text; active : bool }) query; + "btree_reset": () -> (); + "btree_set": (text, text) -> (); + "btree_get": (text) -> (text) query; + "btree_hasKey": (text) -> (bool) query; + "btree_len": () -> (nat) query; + "btree_range": (text, text) -> (vec record { key : text; value : text }) query; }; diff --git a/examples/stable_memory/backend/src/main.nim b/examples/stable_memory/backend/src/main.nim index 5de2a0e..2b9f5db 100644 --- a/examples/stable_memory/backend/src/main.nim +++ b/examples/stable_memory/backend/src/main.nim @@ -3,6 +3,8 @@ import ../../../../src/nicp_cdk import ../../../../src/nicp_cdk/storage/stable_value import ../../../../src/nicp_cdk/storage/stable_seq import ../../../../src/nicp_cdk/storage/stable_table +import ../../../../src/nicp_cdk/storage/stable_btree +import ../../../../src/nicp_cdk/storage/memory_view # Define base offsets for each storage structure to avoid collision const @@ -18,6 +20,8 @@ const SeqIntDbOffset = 900000'u64 TableDbOffset = 2000000'u64 ObjectDbOffset = 3000000'u64 + BTreeDbOffset = 4000000'u64 + BTreeDbLimit = 1000000'u64 # ================================================== # int @@ -204,7 +208,7 @@ proc seqInt_values() {.query.} = # ================================================== # Table[principal, string] # ================================================== -var tableDb = initIcStableTable[Principal, string](TableDbOffset) +var tableDb = initIcStableTable[Principal, string](TableDbOffset, limit = 1000000'u64) proc table_reset() {.update.} = tableDb.clear() @@ -262,7 +266,7 @@ type UserProfile = object name: string active: bool -var objectDb = initIcStableTable[Principal, UserProfile](ObjectDbOffset) +var objectDb = initIcStableTable[Principal, UserProfile](ObjectDbOffset, limit = 1000000'u64) proc object_set() {.update.} = try: @@ -281,3 +285,63 @@ proc object_get() {.query.} = let principal = Msg.caller() let value = objectDb[principal] reply(value) + +# ================================================== +# IcStableBTreeMap[string, string] +# ================================================== +# This map keeps its searchable index in stable memory. The bounded view +# isolates it from the legacy stable values/tables above, while `range` shows +# the key-order traversal provided by the B+Tree backend. +type BTreeEntry = object + key: string + value: string + +var btreeDb = initIcStableBTreeMap[string, string]( + initRawMemoryView(BTreeDbOffset, BTreeDbLimit) +) + +proc btree_reset() {.update.} = + btreeDb.clear() + reply() + +proc btree_set() {.update.} = + try: + icEcho("btree_set: begin") + let request = Request.new() + icEcho("btree_set: request decoded") + let key = request.getStr(0) + let value = request.getStr(1) + icEcho("btree_set: writing key=", key) + btreeDb[key] = value + icEcho("btree_set: write complete") + reply() + icEcho("btree_set: reply sent") + except Exception as e: + ## The runtime reports uncaught Nim exceptions as a generic IC trap. Keep + ## the concrete reason in canister logs for malformed requests or a + ## corrupted/overlapping stable-memory region. + icEcho("btree_set failed: ", e.msg) + raise + +proc btree_get() {.query.} = + let request = Request.new() + let key = request.getStr(0) + reply(btreeDb[key]) + +proc btree_hasKey() {.query.} = + let request = Request.new() + reply(btreeDb.hasKey(request.getStr(0))) + +proc btree_len() {.query.} = + reply(uint(btreeDb.len())) + +proc btree_range() {.query.} = + ## Returns entries in ascending key order for the half-open interval + ## `[startKey, endKey)`. + let request = Request.new() + let startKey = request.getStr(0) + let endKey = request.getStr(1) + var entries: seq[BTreeEntry] = @[] + for key, value in btreeDb.range(startKey, endKey): + entries.add(BTreeEntry(key: key, value: value)) + reply(entries) diff --git a/examples/stable_memory/src/stable_memory_backend/main.nim b/examples/stable_memory/src/stable_memory_backend/main.nim index 5de2a0e..6583483 100644 --- a/examples/stable_memory/src/stable_memory_backend/main.nim +++ b/examples/stable_memory/src/stable_memory_backend/main.nim @@ -204,7 +204,7 @@ proc seqInt_values() {.query.} = # ================================================== # Table[principal, string] # ================================================== -var tableDb = initIcStableTable[Principal, string](TableDbOffset) +var tableDb = initIcStableTable[Principal, string](TableDbOffset, limit = 1000000'u64) proc table_reset() {.update.} = tableDb.clear() @@ -262,7 +262,7 @@ type UserProfile = object name: string active: bool -var objectDb = initIcStableTable[Principal, UserProfile](ObjectDbOffset) +var objectDb = initIcStableTable[Principal, UserProfile](ObjectDbOffset, limit = 1000000'u64) proc object_set() {.update.} = try: diff --git a/src/nicp_cdk/request.nim b/src/nicp_cdk/request.nim index bf8735a..0809fd9 100644 --- a/src/nicp_cdk/request.nim +++ b/src/nicp_cdk/request.nim @@ -47,6 +47,23 @@ proc getNat32*(self:Request, index:int): uint32 = assert self.values[index].kind == ctNat32, "Expected nat32 type, got: " & $self.values[index].kind return self.values[index].nat32Val +proc getNat32Compatible*(self: Request, index: int): uint32 = + ## Get a key represented as either Candid `nat32` or `nat`. + ## + ## Storage APIs commonly use a fixed-width uint32 key, while older callers + ## may still send an unbounded `nat`. Accept both forms explicitly instead + ## of relying on an assertion that would trap the canister without context. + case self.values[index].kind + of ctNat32: + result = self.values[index].nat32Val + of ctNat: + if self.values[index].natVal > uint(high(uint32)): + raise newException(ValueError, "nat key exceeds uint32 range") + result = uint32(self.values[index].natVal) + else: + raise newException(ValueError, + "Expected nat32 or nat key, got: " & $self.values[index].kind) + proc getNat64*(self:Request, index:int): uint64 = ## Get the argument at the specified index as a nat64 diff --git a/src/nicp_cdk/storage/linear_hashing.nim b/src/nicp_cdk/storage/linear_hashing.nim new file mode 100644 index 0000000..cde0b06 --- /dev/null +++ b/src/nicp_cdk/storage/linear_hashing.nim @@ -0,0 +1,36 @@ +## Persistent linear-hashing state transitions. +type LinearHashState* = object + level*, split*: uint32 + +proc validate(state: LinearHashState) {.inline.} = + ## `split` is persisted as uint32, so level 32 is the largest representable + ## round. Keeping this check at the state boundary prevents a corrupt header + ## from turning into an overflowing split counter or shift operation. + if state.level > 32 or state.split >= (1'u64 shl state.level): + raise newException(ValueError, "invalid linear hash state") + +proc bucketIndex*(state: LinearHashState, hash: uint64): uint64 = + state.validate + let baseMask = (1'u64 shl state.level) - 1 + result = hash and baseMask + if result < uint64(state.split): + result = hash and ((1'u64 shl (state.level + 1)) - 1) + +proc bucketCount*(state: LinearHashState): uint64 = + state.validate + (1'u64 shl state.level) + uint64(state.split) + +proc splitBucket*(state: LinearHashState): uint64 = + state.validate + uint64(state.split) + +proc advanceSplit*(state: var LinearHashState) = + state.validate + if state.level == 32 and state.split == high(uint32): + raise newException(ValueError, "linear hash level cannot grow further") + inc state.split + if uint64(state.split) == (1'u64 shl state.level): + if state.level == 32: + raise newException(ValueError, "linear hash level cannot grow further") + inc state.level + state.split = 0 diff --git a/src/nicp_cdk/storage/memory_manager.nim b/src/nicp_cdk/storage/memory_manager.nim new file mode 100644 index 0000000..0808497 --- /dev/null +++ b/src/nicp_cdk/storage/memory_manager.nim @@ -0,0 +1,152 @@ +## Persistent bucket-based virtual memory manager for stable memory. +## Each virtual memory owns a linked list of fixed-size physical buckets, so +## independently growing structures cannot overwrite one another. + +import std/endians +import ./stable_memory +import ./memory_view + +const + MemoryManagerMagic = [byte('V'), byte('M'), byte('M'), byte('2')] + MemoryManagerVersion* = 1'u32 + MaxVirtualMemories* = 255 + ManagerHeaderSize = 8192'u64 + DescriptorSize = 24'u64 + DescriptorStart = 32'u64 + BucketHeaderSize = 8'u64 + DefaultBucketSize* = 65536'u32 + +type + VirtualDescriptor = object + head, tail, size: uint64 + MemoryManager* = ref object + baseOffset: uint64 + bucketSize: uint32 + nextPhysical: uint64 + descriptors: array[MaxVirtualMemories, VirtualDescriptor] + VirtualMemory* = object + manager: MemoryManager + memoryId*: uint8 + limit*: uint64 + +proc put32(data: var openArray[byte], at: int, value: uint32) = + var le = value; littleEndian32(addr data[at], addr le) +proc put64(data: var openArray[byte], at: int, value: uint64) = + var le = value; littleEndian64(addr data[at], addr le) +proc get32(data: openArray[byte], at: int): uint32 = + littleEndian32(addr result, unsafeAddr data[at]) +proc get64(data: openArray[byte], at: int): uint64 = + littleEndian64(addr result, unsafeAddr data[at]) + +proc writeManagerHeader(manager: MemoryManager) = + var data = newSeq[byte](int(ManagerHeaderSize)) + for i in 0 .. 3: data[i] = MemoryManagerMagic[i] + data.put32(4, MemoryManagerVersion); data.put32(8, manager.bucketSize); data.put64(16, manager.nextPhysical) + for index in 0 ..< MaxVirtualMemories: + let offset = int(DescriptorStart + uint64(index) * DescriptorSize) + data.put64(offset, manager.descriptors[index].head) + data.put64(offset + 8, manager.descriptors[index].tail) + data.put64(offset + 16, manager.descriptors[index].size) + stableWrite(manager.baseOffset, data) + +proc initMemoryManager*(baseOffset: uint64, bucketSize: uint32 = DefaultBucketSize): MemoryManager = + if bucketSize <= uint32(BucketHeaderSize) or bucketSize mod StablePageSize.uint32 != 0: + raise newException(ValueError, "bucketSize must be a stable-memory page multiple") + new(result); result.baseOffset = baseOffset + if stableSizeBytes() >= baseOffset + ManagerHeaderSize: + let data = stableRead(baseOffset, ManagerHeaderSize) + var valid = true + for i in 0 .. 3: valid = valid and data[i] == MemoryManagerMagic[i] + if valid: + if data.get32(4) != MemoryManagerVersion: raise newException(ValueError, "unsupported virtual memory layout") + result.bucketSize = data.get32(8) + if result.bucketSize != bucketSize: raise newException(ValueError, "virtual memory bucket size mismatch") + result.nextPhysical = data.get64(16) + if result.nextPhysical < baseOffset + ManagerHeaderSize or result.nextPhysical > stableSizeBytes(): + raise newException(ValueError, "invalid virtual memory allocator metadata") + for index in 0 ..< MaxVirtualMemories: + let offset = int(DescriptorStart + uint64(index) * DescriptorSize) + result.descriptors[index] = VirtualDescriptor(head: data.get64(offset), tail: data.get64(offset + 8), size: data.get64(offset + 16)) + return + result.bucketSize = bucketSize + result.nextPhysical = baseOffset + ManagerHeaderSize + result.writeManagerHeader + +proc payloadSize(manager: MemoryManager): uint64 = uint64(manager.bucketSize) - BucketHeaderSize + +proc appendBucket(manager: MemoryManager, descriptorIndex: int): uint64 = + result = manager.nextPhysical + manager.nextPhysical += uint64(manager.bucketSize) + var header = newSeq[byte](8); header.put64(0, 0) + stableWrite(result, header) + if manager.descriptors[descriptorIndex].tail == 0: + manager.descriptors[descriptorIndex].head = result + else: + var tailHeader = newSeq[byte](8); tailHeader.put64(0, result) + stableWrite(manager.descriptors[descriptorIndex].tail, tailHeader) + manager.descriptors[descriptorIndex].tail = result + manager.writeManagerHeader + +proc bucketAt(manager: MemoryManager, memoryId: uint8, index: uint64, create: bool): uint64 = + let descriptorIndex = int(memoryId) + var current = manager.descriptors[descriptorIndex].head + if current == 0: + if not create: raise newException(ValueError, "virtual memory bucket is missing") + current = manager.appendBucket(descriptorIndex) + for _ in 0 ..< index: + let header = stableRead(current, 8) + let next = header.get64(0) + if next == 0: + if not create: raise newException(ValueError, "virtual memory bucket is missing") + current = manager.appendBucket(descriptorIndex) + else: + current = next + current + +proc writeVirtual(memory: VirtualMemory, offset: uint64, data: seq[byte]) = + if data.len == 0: return + let endOffset = offset + uint64(data.len) + if endOffset < offset: raise newException(ValueError, "virtual memory address overflow") + let payload = memory.manager.payloadSize + var position = offset; var sourcePosition = 0 + while sourcePosition < data.len: + let bucketIndex = position div payload + let inBucket = position mod payload + let count = min(uint64(data.len - sourcePosition), payload - inBucket) + let bucket = memory.manager.bucketAt(memory.memoryId, bucketIndex, true) + stableWrite(bucket + BucketHeaderSize + inBucket, data[sourcePosition ..< sourcePosition + int(count)]) + position += count; sourcePosition += int(count) + let descriptorIndex = int(memory.memoryId) + if endOffset > memory.manager.descriptors[descriptorIndex].size: + memory.manager.descriptors[descriptorIndex].size = endOffset + memory.manager.writeManagerHeader + +proc readVirtual(memory: VirtualMemory, offset, size: uint64): seq[byte] = + let descriptor = memory.manager.descriptors[int(memory.memoryId)] + if offset > descriptor.size or size > descriptor.size - offset: + raise newException(ValueError, "virtual memory read exceeds allocated size") + result = newSeq[byte](int(size)) + let payload = memory.manager.payloadSize + var position = offset; var destinationPosition = 0 + while destinationPosition < result.len: + let bucketIndex = position div payload + let inBucket = position mod payload + let count = min(uint64(result.len - destinationPosition), payload - inBucket) + let bucket = memory.manager.bucketAt(memory.memoryId, bucketIndex, false) + let part = stableRead(bucket + BucketHeaderSize + inBucket, count) + for i, value in part: result[destinationPosition + i] = value + position += count; destinationPosition += int(count) + +proc initVirtualMemory*(manager: MemoryManager, memoryId: uint8, limit: uint64 = 0): VirtualMemory = + VirtualMemory(manager: manager, memoryId: memoryId, limit: limit) + +proc size*(memory: VirtualMemory): uint64 = memory.manager.descriptors[int(memory.memoryId)].size + +proc view*(memory: VirtualMemory): StableMemoryView = + let captured = memory + initMemoryView( + proc(): uint64 = captured.size, + proc(offset, size: uint64): seq[byte] = captured.readVirtual(offset, size), + proc(offset: uint64, data: seq[byte]) = captured.writeVirtual(offset, data), + captured.limit + ) diff --git a/src/nicp_cdk/storage/memory_view.nim b/src/nicp_cdk/storage/memory_view.nim new file mode 100644 index 0000000..70ad19b --- /dev/null +++ b/src/nicp_cdk/storage/memory_view.nim @@ -0,0 +1,84 @@ +## A bounded logical view over IC stable memory. +## +## Keeping the bound in this type makes accidental reads into another +## structure fail before they reach the ic0 API. + +when not defined(nicpMemoryViewOnly): + import ./stable_memory + +when defined(nicpMemoryViewOnly): + proc rawStableSize(): uint64 = 0 + proc rawStableReadInto(dst: var openArray[byte], offset: uint64) = + discard dst; discard offset + raise newException(ValueError, "RawMemoryView is unavailable in memory-view-only builds") + proc rawStableWrite(offset: uint64, data: openArray[byte]) = + discard offset; discard data + raise newException(ValueError, "RawMemoryView is unavailable in memory-view-only builds") +else: + proc rawStableSize(): uint64 = stableSizeBytes() + proc rawStableReadInto(dst: var openArray[byte], offset: uint64) = stableReadInto(dst, offset) + proc rawStableWrite(offset: uint64, data: openArray[byte]) = stableWrite(offset, data) + +type StableMemoryView* = object + baseOffset*: uint64 + limit*: uint64 # 0 means the view may grow without an explicit limit. + sizeProc: proc(): uint64 {.closure.} + readProc: proc(offset, size: uint64): seq[byte] {.closure.} + writeProc: proc(offset: uint64, data: seq[byte]) {.closure.} + +proc initRawMemoryView*(baseOffset: uint64 = 0, limit: uint64 = 0): StableMemoryView = + StableMemoryView(baseOffset: baseOffset, limit: limit) + +proc initMemoryView*(sizeProc: proc(): uint64 {.closure.}, + readProc: proc(offset, size: uint64): seq[byte] {.closure.}, + writeProc: proc(offset: uint64, data: seq[byte]) {.closure.}, + limit: uint64 = 0): StableMemoryView = + ## Test/alternate backend constructor. Offsets are relative to this view. + StableMemoryView(limit: limit, sizeProc: sizeProc, readProc: readProc, writeProc: writeProc) + +proc checkRange(view: StableMemoryView, offset, size: uint64) = + if offset > high(uint64) - size: + raise newException(ValueError, "stable memory address overflow") + if view.limit != 0 and (offset > view.limit or size > view.limit - offset): + raise newException(ValueError, "stable memory view bounds exceeded") + +proc readInto*(view: StableMemoryView, dst: var openArray[byte], offset: uint64) = + view.checkRange(offset, uint64(dst.len)) + if not view.readProc.isNil: + let data = view.readProc(offset, uint64(dst.len)) + if data.len != dst.len: raise newException(ValueError, "memory backend returned an invalid read length") + for i in 0 ..< dst.len: dst[i] = data[i] + return + rawStableReadInto(dst, view.baseOffset + offset) + +proc read*(view: StableMemoryView, offset, size: uint64): seq[byte] = + view.checkRange(offset, size) + var available = if view.sizeProc.isNil: rawStableSize() else: view.sizeProc() + if view.sizeProc.isNil: + if available <= view.baseOffset: available = 0 else: available -= view.baseOffset + if view.limit != 0 and available > view.limit: available = view.limit + if offset > available or size > available - offset: + raise newException(ValueError, "stable memory read exceeds allocated size") + if not view.readProc.isNil: + result = view.readProc(offset, size) + if result.len != int(size): raise newException(ValueError, "memory backend returned an invalid read length") + else: + result = newSeq[byte](int(size)) + rawStableReadInto(result, view.baseOffset + offset) + +proc write*(view: StableMemoryView, offset: uint64, data: openArray[byte]) = + view.checkRange(offset, uint64(data.len)) + if not view.writeProc.isNil: + view.writeProc(offset, @data) + else: + rawStableWrite(view.baseOffset + offset, data) + +proc size*(view: StableMemoryView): uint64 = + let physical = if view.sizeProc.isNil: rawStableSize() else: view.sizeProc() + if not view.sizeProc.isNil: + result = physical + if view.limit != 0 and result > view.limit: result = view.limit + return + if physical <= view.baseOffset: return 0 + result = physical - view.baseOffset + if view.limit != 0 and result > view.limit: result = view.limit diff --git a/src/nicp_cdk/storage/stable_allocator.nim b/src/nicp_cdk/storage/stable_allocator.nim new file mode 100644 index 0000000..1577948 --- /dev/null +++ b/src/nicp_cdk/storage/stable_allocator.nim @@ -0,0 +1,22 @@ +## Persistent append allocator used by the first SBT2 layout. +## Free-list reuse is intentionally kept behind this module's API so the +## on-disk header can gain bins without changing tree code. + +import ./memory_view + +type StableAllocator* = object + arenaEnd*: uint64 + +proc initStableAllocator*(arenaEnd: uint64): StableAllocator = + StableAllocator(arenaEnd: arenaEnd) + +proc allocate*(allocator: var StableAllocator, view: StableMemoryView, + size, alignment: uint64): uint64 = + if alignment == 0 or (alignment and (alignment - 1)) != 0: + raise newException(ValueError, "alignment must be a power of two") + let padding = (alignment - (allocator.arenaEnd and (alignment - 1))) and (alignment - 1) + if allocator.arenaEnd > high(uint64) - padding - size: + raise newException(ValueError, "stable allocator overflow") + result = allocator.arenaEnd + padding + discard view # view bounds are enforced by the first write. + allocator.arenaEnd = result + size diff --git a/src/nicp_cdk/storage/stable_btree.nim b/src/nicp_cdk/storage/stable_btree.nim new file mode 100644 index 0000000..7653f4f --- /dev/null +++ b/src/nicp_cdk/storage/stable_btree.nim @@ -0,0 +1,401 @@ +## Stable-memory-native B+Tree. Only the superblock is loaded at open time; +## nodes and values are read by address on demand. + +import std/endians +import std/options +import ./serialization +import ./memory_view +import ./stable_allocator +import ./stable_key_codec +import ../ic_types/ic_principal + +const + BTreeMagic = [byte('S'), byte('B'), byte('T'), byte('2')] + BTreeVersion* = 3'u16 + SuperblockSize = 256'u64 + DefaultNodeSize* = 1024'u32 + NodeHeaderSize = 48 + SlotSize = 32 + LeafNode = 1'u8 + InternalNode = 2'u8 + +type + BTreeHeader = object + count, rootAddr, firstLeaf, lastLeaf, blobFreeHead, arenaEnd: uint64 + height: uint32 + Slot = object + keyOff: uint64 + keyLen: uint32 + valueOff: uint64 + valueLen: uint32 + child: uint64 + Node = object + kind: uint8 + prev, next, firstChild: uint64 + slots: seq[Slot] + NodeCacheEntry = object + address: uint64 + node: Node + valid: bool + NodeCache = ref object + entries: seq[NodeCacheEntry] + StableKeyCodec*[K] = object + id*: uint32 + encode*: proc(key: K): seq[byte] {.closure.} + decode*: proc(data: openArray[byte]): K {.closure.} + IcStableBTreeMap*[K, V] = object + memory: StableMemoryView + header: BTreeHeader + nodeSize: uint32 + keyCodecId: uint32 + valueCodecId: uint32 + cache: NodeCache + codec: StableKeyCodec[K] + builtinCodec: bool + +proc put32(data: var openArray[byte], at: int, value: uint32) = + var x = value; littleEndian32(addr data[at], addr x) +proc put64(data: var openArray[byte], at: int, value: uint64) = + var x = value; littleEndian64(addr data[at], addr x) +proc get32(data: openArray[byte], at: int): uint32 = + littleEndian32(addr result, unsafeAddr data[at]) +proc get64(data: openArray[byte], at: int): uint64 = + littleEndian64(addr result, unsafeAddr data[at]) +proc capacity(t: IcStableBTreeMap): int = (int(t.nodeSize) - NodeHeaderSize) div SlotSize + +proc encodeKey[K, V](t: IcStableBTreeMap[K, V], key: K): seq[byte] = + ## Avoid an indirect closure call for built-in codecs. Besides reducing the + ## hot-path overhead, this keeps the default codec path compatible with the + ## WASM ABI used by the example canister. + when K is string: + return stableKeyEncode(key) + elif K is bool or K is char or K is SomeUnsignedInt or K is SomeSignedInt or K is Principal: + if t.keyCodecId == stableKeyCodecId(K): return stableKeyEncode(key) + t.codec.encode(key) + +proc decodeKey[K, V](t: IcStableBTreeMap[K, V], data: openArray[byte]): K = + when K is string: + return stableKeyDecode[K](data) + elif K is bool or K is char or K is SomeUnsignedInt or K is SomeSignedInt or K is Principal: + if t.keyCodecId == stableKeyCodecId(K): return stableKeyDecode[K](data) + t.codec.decode(data) + +proc writeHeader[K, V](t: IcStableBTreeMap[K, V]) = + var b = newSeq[byte](int(SuperblockSize)) + for i in 0 .. 3: b[i] = BTreeMagic[i] + b.put32(4, uint32(BTreeVersion)); b.put32(8, t.nodeSize) + b.put32(12, t.keyCodecId); b.put32(16, t.valueCodecId) + b.put64(24, t.header.count); b.put64(32, t.header.rootAddr) + b.put32(40, t.header.height); b.put64(48, t.header.firstLeaf) + b.put64(56, t.header.lastLeaf); b.put64(64, t.header.blobFreeHead); b.put64(72, t.header.arenaEnd) + t.memory.write(0, b) + +proc readHeader[K, V](t: var IcStableBTreeMap[K, V]): bool = + if t.memory.size < SuperblockSize: return false + let b = t.memory.read(0, SuperblockSize) + for i in 0 .. 3: + if b[i] != BTreeMagic[i]: return false + if b.get32(4) != uint32(BTreeVersion): + raise newException(ValueError, "unsupported SBT2 layout version") + t.nodeSize = b.get32(8) + if t.nodeSize < 256 or (t.nodeSize and (t.nodeSize - 1)) != 0: + raise newException(ValueError, "invalid SBT2 node size") + let storedKeyCodecId = b.get32(12) + if storedKeyCodecId != t.keyCodecId: raise newException(ValueError, "SBT2 key codec mismatch") + let storedValueCodecId = b.get32(16) + if storedValueCodecId != t.valueCodecId: raise newException(ValueError, "SBT2 value codec mismatch") + t.keyCodecId = storedKeyCodecId; t.valueCodecId = storedValueCodecId + t.header.count = b.get64(24); t.header.rootAddr = b.get64(32) + t.header.height = b.get32(40); t.header.firstLeaf = b.get64(48); t.header.lastLeaf = b.get64(56); t.header.blobFreeHead = b.get64(64) + t.header.arenaEnd = b.get64(72) + if t.header.arenaEnd < SuperblockSize or t.header.arenaEnd > t.memory.size: + raise newException(ValueError, "invalid SBT2 allocator metadata") + result = true + +proc readNode[K, V](t: IcStableBTreeMap[K, V], address: uint64): Node = + if not t.cache.isNil and t.cache.entries.len > 0: + let slot = int((address div uint64(t.nodeSize)) mod uint64(t.cache.entries.len)) + let cached = t.cache.entries[slot] + if cached.valid and cached.address == address: return cached.node + if address < SuperblockSize or address > t.memory.size - uint64(t.nodeSize): + raise newException(ValueError, "SBT2 node address out of bounds") + let b = t.memory.read(address, uint64(t.nodeSize)) + result.kind = b[0] + if result.kind != LeafNode and result.kind != InternalNode: raise newException(ValueError, "invalid SBT2 node type") + let n = int(b.get32(4)) + if n > t.capacity: raise newException(ValueError, "invalid SBT2 node slot count") + result.prev = b.get64(8); result.next = b.get64(16); result.firstChild = b.get64(24) + result.slots = newSeq[Slot](n) + for i in 0 ..< n: + let p = NodeHeaderSize + i * SlotSize + result.slots[i] = Slot(keyOff: b.get64(p), keyLen: b.get32(p + 8), valueOff: b.get64(p + 12), valueLen: b.get32(p + 20), child: b.get64(p + 24)) + if not t.cache.isNil and t.cache.entries.len > 0: + let slot = int((address div uint64(t.nodeSize)) mod uint64(t.cache.entries.len)) + t.cache.entries[slot] = NodeCacheEntry(address: address, node: result, valid: true) + +proc writeNode[K, V](t: IcStableBTreeMap[K, V], address: uint64, node: Node) = + if node.slots.len > t.capacity: raise newException(ValueError, "SBT2 node overflow") + var b = newSeq[byte](int(t.nodeSize)); b[0] = node.kind; b.put32(4, uint32(node.slots.len)) + b.put64(8, node.prev); b.put64(16, node.next); b.put64(24, node.firstChild) + for i, s in node.slots: + let p = NodeHeaderSize + i * SlotSize + b.put64(p, s.keyOff); b.put32(p + 8, s.keyLen); b.put64(p + 12, s.valueOff); b.put32(p + 20, s.valueLen); b.put64(p + 24, s.child) + t.memory.write(address, b) + if not t.cache.isNil and t.cache.entries.len > 0: + let slot = int((address div uint64(t.nodeSize)) mod uint64(t.cache.entries.len)) + t.cache.entries[slot] = NodeCacheEntry(address: address, node: node, valid: true) + +proc alloc[K, V](t: var IcStableBTreeMap[K, V], size, alignment: uint64): uint64 = + var a = initStableAllocator(t.header.arenaEnd) + result = a.allocate(t.memory, size, alignment); t.header.arenaEnd = a.arenaEnd +proc readBlobHeader[K, V](t: IcStableBTreeMap[K, V], address: uint64): (uint64, uint64) = + if address < SuperblockSize or address > t.memory.size - 16'u64: + raise newException(ValueError, "SBT2 blob header out of bounds") + let data = t.memory.read(address, 16) + (data.get64(0), data.get64(8)) # payload capacity, next free header + +proc writeBlobHeader[K, V](t: IcStableBTreeMap[K, V], address, capacity, next: uint64) = + var data = newSeq[byte](16); data.put64(0, capacity); data.put64(8, next) + t.memory.write(address, data) + +proc writeBlob[K, V](t: var IcStableBTreeMap[K, V], data: openArray[byte]): uint64 = + ## A first-fit free-list keeps update-heavy tables from becoming append-only. + var previous = 0'u64 + var current = t.header.blobFreeHead + while current != 0: + let (capacity, next) = t.readBlobHeader(current) + if capacity >= uint64(data.len): + if previous == 0: t.header.blobFreeHead = next + else: + let (previousCapacity, _) = t.readBlobHeader(previous) + t.writeBlobHeader(previous, previousCapacity, next) + result = current + 16'u64 + t.memory.write(result, data) + return + previous = current; current = next + let headerAddress = t.alloc(16'u64 + uint64(data.len), 8) + t.writeBlobHeader(headerAddress, uint64(data.len), 0) + result = headerAddress + 16'u64 + t.memory.write(result, data) + +proc freeBlob[K, V](t: var IcStableBTreeMap[K, V], payloadAddress: uint64) = + if payloadAddress < SuperblockSize + 16'u64: + raise newException(ValueError, "invalid SBT2 blob address") + let headerAddress = payloadAddress - 16'u64 + let (capacity, _) = t.readBlobHeader(headerAddress) + t.writeBlobHeader(headerAddress, capacity, t.header.blobFreeHead) + t.header.blobFreeHead = headerAddress +proc readKey[K, V](t: IcStableBTreeMap[K, V], s: Slot): seq[byte] = + if s.keyOff > t.memory.size or uint64(s.keyLen) > t.memory.size - s.keyOff: raise newException(ValueError, "SBT2 key blob out of bounds") + t.memory.read(s.keyOff, uint64(s.keyLen)) +proc bytesCompare(a, b: openArray[byte]): int = + for i in 0 ..< min(a.len, b.len): + if a[i] != b[i]: return (if a[i] < b[i]: -1 else: 1) + system.cmp(a.len, b.len) +proc lower[K, V](t: IcStableBTreeMap[K, V], n: Node, key: openArray[byte]): int = + var lo = 0; var hi = n.slots.len + while lo < hi: + let mid = (lo + hi) div 2 + if bytesCompare(t.readKey(n.slots[mid]), key) < 0: lo = mid + 1 else: hi = mid + lo +proc childIndex[K, V](t: IcStableBTreeMap[K, V], n: Node, key: openArray[byte]): int = + ## Internal separators are the first key of their right child, so equality + ## must select that right child (upper-bound semantics). + var lo = 0; var hi = n.slots.len + while lo < hi: + let mid = (lo + hi) div 2 + if bytesCompare(t.readKey(n.slots[mid]), key) <= 0: lo = mid + 1 else: hi = mid + lo +proc newNode[K, V](t: var IcStableBTreeMap[K, V], kind: uint8): uint64 = + result = t.alloc(uint64(t.nodeSize), uint64(t.nodeSize)); t.writeNode(result, Node(kind: kind)) + +proc initIcStableBTreeMap*[K, V](memory: StableMemoryView, codec: StableKeyCodec[K], cacheSlots: int = 16, + valueCodecId: uint32 = 0): IcStableBTreeMap[K, V] = + if cacheSlots < 0: raise newException(ValueError, "cacheSlots must not be negative") + if codec.id == 0 or codec.encode.isNil or codec.decode.isNil: raise newException(ValueError, "invalid StableKeyCodec") + result.memory = memory; result.nodeSize = DefaultNodeSize; result.keyCodecId = codec.id; result.valueCodecId = valueCodecId; result.codec = codec + if not result.readHeader: + if memory.size >= 4 and memory.read(0, 4) == @[byte('S'), byte('T'), byte('B'), byte('L')]: + raise newException(ValueError, "STBL v1 detected: explicit migration is required") + result.header.arenaEnd = SuperblockSize + result.writeHeader + if cacheSlots > 0: + new(result.cache) + result.cache.entries = newSeq[NodeCacheEntry](cacheSlots) + +proc initIcStableBTreeMap*[K, V](memory: StableMemoryView = initRawMemoryView(), cacheSlots: int = 16, + valueCodecId: uint32 = 0): IcStableBTreeMap[K, V] = + let codec = StableKeyCodec[K](id: stableKeyCodecId(K), + encode: proc(key: K): seq[byte] = stableKeyEncode(key), + decode: proc(data: openArray[byte]): K = stableKeyDecode[K](data)) + result = initIcStableBTreeMap[K, V](memory, codec, cacheSlots, valueCodecId) + result.builtinCodec = true + +proc len*[K, V](t: IcStableBTreeMap[K, V]): int = int(t.header.count) +proc hasKey*[K, V](t: IcStableBTreeMap[K, V], key: K): bool {.noinline.} = + let q = t.encodeKey(key); var nodeAddr = t.header.rootAddr + while nodeAddr != 0: + let n = t.readNode(nodeAddr) + if n.kind == LeafNode: + let i = t.lower(n, q) + return i < n.slots.len and bytesCompare(t.readKey(n.slots[i]), q) == 0 + let i = t.childIndex(n, q) + nodeAddr = if i == 0: n.firstChild else: n.slots[i - 1].child + false + +proc `[]`*[K, V](t: IcStableBTreeMap[K, V], key: K): V {.noinline.} = + let q = t.encodeKey(key); var nodeAddr = t.header.rootAddr + while nodeAddr != 0: + let n = t.readNode(nodeAddr) + if n.kind == LeafNode: + let i = t.lower(n, q) + if i == n.slots.len or bytesCompare(t.readKey(n.slots[i]), q) != 0: raise newException(KeyError, "key not found") + let s = n.slots[i]; let data = t.memory.read(s.valueOff, uint64(s.valueLen)); var p = 0; return deserialize[V](data, p) + let i = t.childIndex(n, q) + nodeAddr = if i == 0: n.firstChild else: n.slots[i - 1].child + raise newException(KeyError, "key not found") + +proc lowerBound*[K, V](t: IcStableBTreeMap[K, V], key: K): Option[(K, V)] = + ## Returns the first entry whose key is not smaller than `key`. + let query = t.encodeKey(key) + var nodeAddr = t.header.rootAddr + while nodeAddr != 0: + let node = t.readNode(nodeAddr) + if node.kind == LeafNode: + let index = t.lower(node, query) + if index == node.slots.len: + nodeAddr = node.next + if nodeAddr == 0: return none((K, V)) + let nextLeaf = t.readNode(nodeAddr) + if nextLeaf.slots.len == 0: return none((K, V)) + let slot = nextLeaf.slots[0] + let valueData = t.memory.read(slot.valueOff, uint64(slot.valueLen)); var valuePos = 0 + return some((t.decodeKey(t.readKey(slot)), deserialize[V](valueData, valuePos))) + let slot = node.slots[index] + let valueData = t.memory.read(slot.valueOff, uint64(slot.valueLen)); var valuePos = 0 + return some((t.decodeKey(t.readKey(slot)), deserialize[V](valueData, valuePos))) + let index = t.childIndex(node, query) + nodeAddr = if index == 0: node.firstChild else: node.slots[index - 1].child + none((K, V)) + +proc insertIntoParent[K, V](t: var IcStableBTreeMap[K, V], path: seq[uint64], childIndexes: seq[int], + separator: Slot, rightAddr: uint64) = + var sep = separator; var right = rightAddr + for level in countdown(path.high, 0): + let parentAddr = path[level]; var parent = t.readNode(parentAddr) + let i = childIndexes[level] + parent.slots.insert(sep, i) + parent.slots[i].child = right + if parent.slots.len <= t.capacity: + t.writeNode(parentAddr, parent); return + let middle = parent.slots.len div 2 + let promoted = parent.slots[middle] + var rightNode = Node(kind: InternalNode, firstChild: promoted.child) + if middle + 1 < parent.slots.len: rightNode.slots = parent.slots[middle + 1 .. ^1] + parent.slots.setLen(middle) + let sibling = t.newNode(InternalNode) + t.writeNode(parentAddr, parent); t.writeNode(sibling, rightNode) + sep = promoted; right = sibling + let root = t.newNode(InternalNode) + let n = Node(kind: InternalNode, firstChild: t.header.rootAddr, slots: @[sep]) + var rootNode = n; rootNode.slots[0].child = right + t.writeNode(root, rootNode); t.header.rootAddr = root; inc t.header.height + +proc `[]=`*[K, V](t: var IcStableBTreeMap[K, V], key: K, value: V) {.noinline.} = + let keyBytes = t.encodeKey(key) + let valueBytes = serialize(value) + if t.header.rootAddr == 0: + let keyOff = t.writeBlob(keyBytes); let valueOff = t.writeBlob(valueBytes) + let root = t.newNode(LeafNode) + t.writeNode(root, Node(kind: LeafNode, slots: @[Slot(keyOff: keyOff, keyLen: uint32(keyBytes.len), valueOff: valueOff, valueLen: uint32(valueBytes.len))])) + t.header.rootAddr = root; t.header.firstLeaf = root; t.header.lastLeaf = root + t.header.height = 1; t.header.count = 1; t.writeHeader + else: + block inserted: + var nodeAddr = t.header.rootAddr; var path: seq[uint64] = @[]; var childIndexes: seq[int] = @[] + while true: + let n = t.readNode(nodeAddr) + if n.kind == LeafNode: break + let i = t.childIndex(n, keyBytes) + path.add(nodeAddr); childIndexes.add(i) + nodeAddr = if i == 0: n.firstChild else: n.slots[i - 1].child + var leaf = t.readNode(nodeAddr); let pos = t.lower(leaf, keyBytes) + let valueOff = t.writeBlob(valueBytes) + if pos < leaf.slots.len and bytesCompare(t.readKey(leaf.slots[pos]), keyBytes) == 0: + let oldValueOff = leaf.slots[pos].valueOff + leaf.slots[pos].valueOff = valueOff; leaf.slots[pos].valueLen = uint32(valueBytes.len) + t.freeBlob(oldValueOff) + t.writeNode(nodeAddr, leaf); t.writeHeader + break inserted + let keyOff = t.writeBlob(keyBytes) + leaf.slots.insert(Slot(keyOff: keyOff, keyLen: uint32(keyBytes.len), valueOff: valueOff, valueLen: uint32(valueBytes.len)), pos) + inc t.header.count + if leaf.slots.len <= t.capacity: + t.writeNode(nodeAddr, leaf); t.writeHeader + break inserted + let cut = leaf.slots.len div 2 + var rightLeaf = Node(kind: LeafNode, prev: nodeAddr, next: leaf.next, slots: leaf.slots[cut .. ^1]) + leaf.slots.setLen(cut) + let rightAddr = t.newNode(LeafNode) + leaf.next = rightAddr + if rightLeaf.next != 0: + var nextLeaf = t.readNode(rightLeaf.next); nextLeaf.prev = rightAddr; t.writeNode(rightLeaf.next, nextLeaf) + else: t.header.lastLeaf = rightAddr + t.writeNode(nodeAddr, leaf); t.writeNode(rightAddr, rightLeaf) + let first = rightLeaf.slots[0] + let separator = Slot(keyOff: first.keyOff, keyLen: first.keyLen) + if path.len == 0: + let root = t.newNode(InternalNode) + t.writeNode(root, Node(kind: InternalNode, firstChild: nodeAddr, slots: @[Slot(keyOff: separator.keyOff, keyLen: separator.keyLen, child: rightAddr)])) + t.header.rootAddr = root; inc t.header.height + else: + t.insertIntoParent(path, childIndexes, separator, rightAddr) + t.writeHeader + +iterator pairs*[K, V](t: IcStableBTreeMap[K, V]): (K, V) = + var nodeAddr = t.header.firstLeaf + while nodeAddr != 0: + let leaf = t.readNode(nodeAddr) + for slot in leaf.slots: + let key = t.decodeKey(t.readKey(slot)) + let data = t.memory.read(slot.valueOff, uint64(slot.valueLen)); var p = 0 + yield (key, deserialize[V](data, p)) + nodeAddr = leaf.next + +iterator keys*[K, V](t: IcStableBTreeMap[K, V]): K = + for key, _ in t.pairs: yield key +iterator values*[K, V](t: IcStableBTreeMap[K, V]): V = + for _, value in t.pairs: yield value + +iterator range*[K, V](t: IcStableBTreeMap[K, V], startKey, endKey: K): (K, V) = + ## Iterates `[startKey, endKey)` in stable-key order. + let start = t.encodeKey(startKey) + let finish = t.encodeKey(endKey) + if bytesCompare(start, finish) < 0: + block done: + var nodeAddr = t.header.rootAddr + while nodeAddr != 0: + let node = t.readNode(nodeAddr) + if node.kind == LeafNode: + let index = t.lower(node, start) + var leaf = node + var slotIndex = index + while true: + while slotIndex < leaf.slots.len: + let slot = leaf.slots[slotIndex] + if bytesCompare(t.readKey(slot), finish) >= 0: break done + let valueData = t.memory.read(slot.valueOff, uint64(slot.valueLen)); var valuePos = 0 + yield (t.decodeKey(t.readKey(slot)), deserialize[V](valueData, valuePos)) + inc slotIndex + if leaf.next == 0: break done + leaf = t.readNode(leaf.next); slotIndex = 0 + let index = t.childIndex(node, start) + nodeAddr = if index == 0: node.firstChild else: node.slots[index - 1].child + +proc clear*[K, V](t: var IcStableBTreeMap[K, V]) = + ## Stable memory cannot shrink; resetting the arena makes this view reusable. + ## Reset the node geometry too: a cleared view must never retain a legacy + ## non-page-aligned node size from an interrupted/older deployment. + t.nodeSize = DefaultNodeSize + t.header = BTreeHeader(arenaEnd: SuperblockSize) + t.writeHeader diff --git a/src/nicp_cdk/storage/stable_hash.nim b/src/nicp_cdk/storage/stable_hash.nim new file mode 100644 index 0000000..90e30b2 --- /dev/null +++ b/src/nicp_cdk/storage/stable_hash.nim @@ -0,0 +1,26 @@ +## Versioned deterministic SipHash-2-4 primitive for stable hash structures. + +const SipHash24Id* = 1'u32 + +type StableHashSeed* = object + k0*, k1*: uint64 + +proc rotl(x: uint64, n: int): uint64 {.inline.} = (x shl n) or (x shr (64 - n)) +template round(v0, v1, v2, v3: untyped) = + v0 = v0 + v1; v1 = rotl(v1, 13); v1 = v1 xor v0; v0 = rotl(v0, 32) + v2 = v2 + v3; v3 = rotl(v3, 16); v3 = v3 xor v2 + v0 = v0 + v3; v3 = rotl(v3, 21); v3 = v3 xor v0 + v2 = v2 + v1; v1 = rotl(v1, 17); v1 = v1 xor v2; v2 = rotl(v2, 32) +proc little(data: openArray[byte], at: int): uint64 = + for i in 0 ..< 8: result = result or (uint64(data[at + i]) shl (8 * i)) +proc sipHash24*(seed: StableHashSeed, data: openArray[byte]): uint64 = + var v0 = 0x736f6d6570736575'u64 xor seed.k0; var v1 = 0x646f72616e646f6d'u64 xor seed.k1 + var v2 = 0x6c7967656e657261'u64 xor seed.k0; var v3 = 0x7465646279746573'u64 xor seed.k1 + var at = 0 + while at + 8 <= data.len: + let m = little(data, at); v3 = v3 xor m; round(v0,v1,v2,v3); round(v0,v1,v2,v3); v0 = v0 xor m; at += 8 + var tail = uint64(data.len) shl 56 + for i in 0 ..< data.len - at: tail = tail or (uint64(data[at + i]) shl (8 * i)) + v3 = v3 xor tail; round(v0,v1,v2,v3); round(v0,v1,v2,v3); v0 = v0 xor tail; v2 = v2 xor 0xff + for _ in 0 ..< 4: round(v0,v1,v2,v3) + v0 xor v1 xor v2 xor v3 diff --git a/src/nicp_cdk/storage/stable_hash_map.nim b/src/nicp_cdk/storage/stable_hash_map.nim new file mode 100644 index 0000000..eddfac6 --- /dev/null +++ b/src/nicp_cdk/storage/stable_hash_map.nim @@ -0,0 +1,297 @@ +## Stable-memory-native exact-match map using linear hashing. +## +## Growth moves one bucket chain at a time; it never rehashes every entry in a +## single operation. The directory is paged so opening the map reads only its +## fixed-size header, independent of the number of buckets or entries. + +import std/endians +import ./serialization +import ./memory_view +import ./linear_hashing +import ./stable_hash + +const + HashMapMagic = [byte('S'), byte('H'), byte('M'), byte('2')] + HashMapVersion* = 1'u32 + HashMapHeaderSize = 256'u64 + DirectoryPageSize = 1024'u64 + DirectoryEntries = 126 + BucketHeaderSize = 16'u64 + EntryHeaderSize = 24'u64 + DefaultBucketLoad* = 8'u32 + DefaultStableHashSeed* = StableHashSeed(k0: 0x0706050403020100'u64, + k1: 0x0f0e0d0c0b0a0908'u64) + +type + HashMapHeader = object + count, directoryHead, directoryTail, arenaEnd: uint64 + state: LinearHashState + Bucket = object + head: uint64 + count: uint64 + HashEntry = object + next, hash: uint64 + keyLen, valueLen: uint32 + IcStableHashMap*[K, V] = object + memory: StableMemoryView + header: HashMapHeader + seed: StableHashSeed + valueCodecId: uint32 + maxBucketLoad: uint32 + +proc put32(data: var openArray[byte], at: int, value: uint32) = + var x = value + littleEndian32(addr data[at], addr x) + +proc put64(data: var openArray[byte], at: int, value: uint64) = + var x = value + littleEndian64(addr data[at], addr x) + +proc get32(data: openArray[byte], at: int): uint32 = + littleEndian32(addr result, unsafeAddr data[at]) + +proc get64(data: openArray[byte], at: int): uint64 = + littleEndian64(addr result, unsafeAddr data[at]) + +proc writeHeader[K, V](t: IcStableHashMap[K, V]) = + var data = newSeq[byte](int(HashMapHeaderSize)) + for i in 0 .. 3: data[i] = HashMapMagic[i] + data.put32(4, HashMapVersion) + data.put32(8, SipHash24Id) + data.put32(12, t.valueCodecId) + data.put64(16, t.seed.k0); data.put64(24, t.seed.k1) + data.put64(32, t.header.count) + data.put32(40, t.header.state.level); data.put32(44, t.header.state.split) + data.put64(48, t.header.directoryHead); data.put64(56, t.header.directoryTail) + data.put64(64, t.header.arenaEnd); data.put32(72, t.maxBucketLoad) + t.memory.write(0, data) + +proc readHeader[K, V](t: var IcStableHashMap[K, V]): bool = + if t.memory.size < HashMapHeaderSize: return false + let data = t.memory.read(0, HashMapHeaderSize) + for i in 0 .. 3: + if data[i] != HashMapMagic[i]: return false + if data.get32(4) != HashMapVersion: raise newException(ValueError, "unsupported SHM2 layout version") + if data.get32(8) != SipHash24Id: raise newException(ValueError, "unsupported stable hash algorithm") + if data.get32(12) != t.valueCodecId: raise newException(ValueError, "SHM2 value codec mismatch") + if data.get64(16) != t.seed.k0 or data.get64(24) != t.seed.k1: + raise newException(ValueError, "SHM2 hash seed mismatch") + t.header.count = data.get64(32) + t.header.state = LinearHashState(level: data.get32(40), split: data.get32(44)) + discard t.header.state.bucketCount # validates persisted state + t.header.directoryHead = data.get64(48); t.header.directoryTail = data.get64(56) + t.header.arenaEnd = data.get64(64); t.maxBucketLoad = data.get32(72) + if t.maxBucketLoad == 0: raise newException(ValueError, "invalid SHM2 bucket load") + if t.header.arenaEnd < HashMapHeaderSize or t.header.arenaEnd > t.memory.size: + raise newException(ValueError, "invalid SHM2 allocator metadata") + if t.header.directoryHead < HashMapHeaderSize or t.header.directoryTail < HashMapHeaderSize: + raise newException(ValueError, "invalid SHM2 directory metadata") + result = true + +proc alloc[K, V](t: var IcStableHashMap[K, V], size: uint64, alignment: uint64 = 8): uint64 = + let mask = alignment - 1 + if alignment == 0 or (alignment and mask) != 0: raise newException(ValueError, "invalid SHM2 alignment") + if t.header.arenaEnd > high(uint64) - mask: raise newException(ValueError, "SHM2 address overflow") + result = (t.header.arenaEnd + mask) and not mask + if size > high(uint64) - result: raise newException(ValueError, "SHM2 allocation overflow") + t.header.arenaEnd = result + size + +proc readBucket[K, V](t: IcStableHashMap[K, V], address: uint64): Bucket = + if address < HashMapHeaderSize or address > t.header.arenaEnd - BucketHeaderSize: + raise newException(ValueError, "SHM2 bucket address out of bounds") + let data = t.memory.read(address, BucketHeaderSize) + result = Bucket(head: data.get64(0), count: data.get64(8)) + +proc writeBucket[K, V](t: IcStableHashMap[K, V], address: uint64, bucket: Bucket) = + var data = newSeq[byte](int(BucketHeaderSize)) + data.put64(0, bucket.head); data.put64(8, bucket.count) + t.memory.write(address, data) + +proc readDirectoryPage[K, V](t: IcStableHashMap[K, V], address: uint64): seq[byte] = + if address < HashMapHeaderSize or address > t.header.arenaEnd - DirectoryPageSize: + raise newException(ValueError, "SHM2 directory page out of bounds") + result = t.memory.read(address, DirectoryPageSize) + if result.get32(8) > DirectoryEntries.uint32: raise newException(ValueError, "invalid SHM2 directory page") + +proc bucketAddress[K, V](t: IcStableHashMap[K, V], index: uint64): uint64 = + if index >= t.header.state.bucketCount: raise newException(ValueError, "SHM2 bucket index out of bounds") + var pageAddress = t.header.directoryHead + var remaining = index + while pageAddress != 0: + let page = t.readDirectoryPage(pageAddress) + let used = uint64(page.get32(8)) + if remaining < used: return page.get64(16 + int(remaining) * 8) + remaining -= used + pageAddress = page.get64(0) + raise newException(ValueError, "truncated SHM2 bucket directory") + +proc appendBucketAddress[K, V](t: var IcStableHashMap[K, V], address: uint64) = + var page = t.readDirectoryPage(t.header.directoryTail) + let used = int(page.get32(8)) + if used < DirectoryEntries: + page.put64(16 + used * 8, address); page.put32(8, uint32(used + 1)) + t.memory.write(t.header.directoryTail, page) + return + let nextPage = t.alloc(DirectoryPageSize) + var next = newSeq[byte](int(DirectoryPageSize)); next.put32(8, 1); next.put64(16, address) + t.memory.write(nextPage, next) + page.put64(0, nextPage); t.memory.write(t.header.directoryTail, page) + t.header.directoryTail = nextPage + +proc newBucket[K, V](t: var IcStableHashMap[K, V]): uint64 = + result = t.alloc(BucketHeaderSize) + t.writeBucket(result, Bucket()) + t.appendBucketAddress(result) + +proc readEntry[K, V](t: IcStableHashMap[K, V], address: uint64): HashEntry = + if address < HashMapHeaderSize or address > t.header.arenaEnd - EntryHeaderSize: + raise newException(ValueError, "SHM2 entry address out of bounds") + let data = t.memory.read(address, EntryHeaderSize) + result = HashEntry(next: data.get64(0), hash: data.get64(8), keyLen: data.get32(16), valueLen: data.get32(20)) + let payload = uint64(result.keyLen) + uint64(result.valueLen) + if payload > t.header.arenaEnd - address - EntryHeaderSize: + raise newException(ValueError, "SHM2 entry payload out of bounds") + +proc writeNext[K, V](t: IcStableHashMap[K, V], address, next: uint64) = + if address < HashMapHeaderSize or address > t.header.arenaEnd - 8'u64: + raise newException(ValueError, "SHM2 entry address out of bounds") + var data = newSeq[byte](8); data.put64(0, next); t.memory.write(address, data) + +proc keyData[K, V](t: IcStableHashMap[K, V], address: uint64, entry: HashEntry): seq[byte] = + t.memory.read(address + EntryHeaderSize, uint64(entry.keyLen)) + +proc valueData[K, V](t: IcStableHashMap[K, V], address: uint64, entry: HashEntry): seq[byte] = + t.memory.read(address + EntryHeaderSize + uint64(entry.keyLen), uint64(entry.valueLen)) + +proc writeEntry[K, V](t: var IcStableHashMap[K, V], next, hash: uint64, + key, value: openArray[byte]): uint64 = + if key.len > int(high(uint32)) or value.len > int(high(uint32)): + raise newException(ValueError, "SHM2 key or value is too large") + result = t.alloc(EntryHeaderSize + uint64(key.len) + uint64(value.len)) + var data = newSeq[byte](int(EntryHeaderSize) + key.len + value.len) + data.put64(0, next); data.put64(8, hash); data.put32(16, uint32(key.len)); data.put32(20, uint32(value.len)) + for i, b in key: data[int(EntryHeaderSize) + i] = b + for i, b in value: data[int(EntryHeaderSize) + key.len + i] = b + t.memory.write(result, data) + +proc findEntry[K, V](t: IcStableHashMap[K, V], key: openArray[byte], hash: uint64): (uint64, uint64, uint64) = + ## Returns bucket address, previous entry, and matching entry (zero if absent). + let bucketAddress = t.bucketAddress(t.header.state.bucketIndex(hash)) + var previous = 0'u64 + var current = t.readBucket(bucketAddress).head + var remaining = t.header.count + 1 + while current != 0: + if remaining == 0: raise newException(ValueError, "cyclic SHM2 bucket chain") + dec remaining + let entry = t.readEntry(current) + if entry.hash == hash and t.keyData(current, entry) == @key: + return (bucketAddress, previous, current) + previous = current; current = entry.next + (bucketAddress, previous, 0) + +proc splitOnce[K, V](t: var IcStableHashMap[K, V]) = + let oldIndex = t.header.state.splitBucket + let oldAddress = t.bucketAddress(oldIndex) + let newIndex = t.header.state.bucketCount + let newAddress = t.newBucket + let oldBucket = t.readBucket(oldAddress) + var oldHead = 0'u64; var oldCount = 0'u64 + var newHead = 0'u64; var newCount = 0'u64 + var current = oldBucket.head + var remaining = oldBucket.count + 1 + while current != 0: + if remaining == 0: raise newException(ValueError, "cyclic SHM2 bucket chain") + dec remaining + let entry = t.readEntry(current) + let next = entry.next + ## This is the bucket currently being split, so routing uses the next + ## level mask even though `state.split` is advanced only after the chain + ## has been rewritten. + let target = entry.hash and ((1'u64 shl (t.header.state.level + 1)) - 1) + if target == oldIndex: + t.writeNext(current, oldHead); oldHead = current; inc oldCount + elif target == newIndex: + t.writeNext(current, newHead); newHead = current; inc newCount + else: + raise newException(ValueError, "invalid SHM2 split routing") + current = next + t.writeBucket(oldAddress, Bucket(head: oldHead, count: oldCount)) + t.writeBucket(newAddress, Bucket(head: newHead, count: newCount)) + t.header.state.advanceSplit + +proc initialize[K, V](t: var IcStableHashMap[K, V]) = + t.header = HashMapHeader(arenaEnd: HashMapHeaderSize, + state: LinearHashState(level: 1, split: 0)) + let directory = t.alloc(DirectoryPageSize) + t.memory.write(directory, newSeq[byte](int(DirectoryPageSize))) + t.header.directoryHead = directory; t.header.directoryTail = directory + discard t.newBucket; discard t.newBucket + t.writeHeader + +proc initIcStableHashMap*[K, V](memory: StableMemoryView = initRawMemoryView(), + seed: StableHashSeed = DefaultStableHashSeed, + maxBucketLoad: uint32 = DefaultBucketLoad, + valueCodecId: uint32 = 0): IcStableHashMap[K, V] = + if maxBucketLoad == 0: raise newException(ValueError, "maxBucketLoad must be positive") + result.memory = memory; result.seed = seed; result.valueCodecId = valueCodecId; result.maxBucketLoad = maxBucketLoad + if result.readHeader: return + result.initialize + +proc len*[K, V](t: IcStableHashMap[K, V]): int = int(t.header.count) + +proc hasKey*[K, V](t: IcStableHashMap[K, V], key: K): bool = + let keyBytes = serialize(key) + let (_, _, found) = t.findEntry(keyBytes, sipHash24(t.seed, keyBytes)) + found != 0 + +proc `[]`*[K, V](t: IcStableHashMap[K, V], key: K): V = + let keyBytes = serialize(key) + let (_, _, found) = t.findEntry(keyBytes, sipHash24(t.seed, keyBytes)) + if found == 0: raise newException(KeyError, "key not found") + let entry = t.readEntry(found) + var position = 0 + deserialize[V](t.valueData(found, entry), position) + +proc `[]=`*[K, V](t: var IcStableHashMap[K, V], key: K, value: V) = + let keyBytes = serialize(key); let valueBytes = serialize(value); let hash = sipHash24(t.seed, keyBytes) + let (bucketAddress, previous, found) = t.findEntry(keyBytes, hash) + if found != 0: + let old = t.readEntry(found) + let replacement = t.writeEntry(old.next, hash, keyBytes, valueBytes) + if previous == 0: + var bucket = t.readBucket(bucketAddress); bucket.head = replacement; t.writeBucket(bucketAddress, bucket) + else: + t.writeNext(previous, replacement) + t.writeHeader + return + var bucket = t.readBucket(bucketAddress) + let entry = t.writeEntry(bucket.head, hash, keyBytes, valueBytes) + bucket.head = entry; inc bucket.count; t.writeBucket(bucketAddress, bucket); inc t.header.count + if t.header.state.bucketCount <= high(uint64) div uint64(t.maxBucketLoad) and + t.header.count > t.header.state.bucketCount * uint64(t.maxBucketLoad): + t.splitOnce + t.writeHeader + +iterator pairs*[K, V](t: IcStableHashMap[K, V]): (K, V) = + for index in 0'u64 ..< t.header.state.bucketCount: + var current = t.readBucket(t.bucketAddress(index)).head + var remaining = t.header.count + 1 + while current != 0: + if remaining == 0: raise newException(ValueError, "cyclic SHM2 bucket chain") + dec remaining + let entry = t.readEntry(current) + var keyPosition = 0; var valuePosition = 0 + yield (deserialize[K](t.keyData(current, entry), keyPosition), + deserialize[V](t.valueData(current, entry), valuePosition)) + current = entry.next + +iterator keys*[K, V](t: IcStableHashMap[K, V]): K = + for key, _ in t.pairs: yield key + +iterator values*[K, V](t: IcStableHashMap[K, V]): V = + for _, value in t.pairs: yield value + +proc clear*[K, V](t: var IcStableHashMap[K, V]) = + ## The physical stable-memory allocation remains, but the logical arena is + ## reset and its first pages are reused. + t.initialize diff --git a/src/nicp_cdk/storage/stable_key_codec.nim b/src/nicp_cdk/storage/stable_key_codec.nim new file mode 100644 index 0000000..253ea9d --- /dev/null +++ b/src/nicp_cdk/storage/stable_key_codec.nim @@ -0,0 +1,98 @@ +## Ordered, versioned encodings for keys used by IcStableBTreeMap. + +import ../ic_types/ic_principal + +const + StringKeyCodecId* = 1'u32 + BoolKeyCodecId* = 2'u32 + UintKeyCodecId* = 10'u32 + IntKeyCodecId* = 20'u32 + PrincipalKeyCodecId* = 30'u32 + +proc putBigEndian(value: uint16): seq[byte] = + @[byte((value shr 8) and 0xff'u16), byte(value and 0xff'u16)] + +proc putBigEndian(value: uint32): seq[byte] = + @[byte((value shr 24) and 0xff'u32), byte((value shr 16) and 0xff'u32), + byte((value shr 8) and 0xff'u32), byte(value and 0xff'u32)] + +proc putBigEndian(value: uint64): seq[byte] = + result = newSeq[byte](8) + for i in 0 ..< 8: + result[i] = byte((value shr (8 * (7 - i))) and 0xff'u64) + +proc stableKeyEncode*(value: string): seq[byte] = + result = newSeq[byte](value.len) + for i in 0 ..< value.len: + result[i] = byte(value[i]) + +proc stableKeyEncode*(value: bool): seq[byte] = @[byte(if value: 1 else: 0)] +proc stableKeyEncode*(value: uint8): seq[byte] = @[byte(value)] +proc stableKeyEncode*(value: int8): seq[byte] = @[byte(cast[uint8](value) xor 0x80'u8)] +proc stableKeyEncode*(value: uint16): seq[byte] = putBigEndian(value) +proc stableKeyEncode*(value: uint32): seq[byte] = putBigEndian(value) +proc stableKeyEncode*(value: uint64): seq[byte] = putBigEndian(value) +proc stableKeyEncode*(value: int16): seq[byte] = putBigEndian(cast[uint16](value) xor 0x8000'u16) +proc stableKeyEncode*(value: int32): seq[byte] = putBigEndian(cast[uint32](value) xor 0x80000000'u32) +proc stableKeyEncode*(value: int64): seq[byte] = putBigEndian(cast[uint64](value) xor 0x8000000000000000'u64) +proc stableKeyEncode*(value: char): seq[byte] = @[byte(value)] +proc stableKeyEncode*(value: Principal): seq[byte] = value.bytes + +proc stableKeyEncode*(value: int): seq[byte] = + when sizeof(int) == 8: stableKeyEncode(int64(value)) + else: stableKeyEncode(int32(value)) +proc stableKeyEncode*(value: uint): seq[byte] = + when sizeof(uint) == 8: stableKeyEncode(uint64(value)) + else: stableKeyEncode(uint32(value)) + +proc stableKeyCodecId*[T](_: typedesc[T]): uint32 = + when T is string: StringKeyCodecId + elif T is bool: BoolKeyCodecId + elif T is Principal: PrincipalKeyCodecId + elif T is SomeUnsignedInt: UintKeyCodecId + uint32(sizeof(T)) + elif T is SomeSignedInt: IntKeyCodecId + uint32(sizeof(T)) + elif T is char: UintKeyCodecId + 1'u32 + else: {.error: "IcStableBTreeMap keys require a StableKeyCodec-supported type".} + +proc stableKeyDecode*[T](data: openArray[byte]): T = + when T is string: + result = newString(data.len) + if data.len > 0: copyMem(addr result[0], unsafeAddr data[0], data.len) + elif T is bool: + if data.len != 1: raise newException(ValueError, "invalid bool key") + result = data[0] != 0 + elif T is uint8: + if data.len != 1: raise newException(ValueError, "invalid uint8 key") + result = data[0] + elif T is int8: + if data.len != 1: raise newException(ValueError, "invalid int8 key") + result = cast[int8](data[0] xor 0x80'u8) + elif T is char: + if data.len != 1: raise newException(ValueError, "invalid char key") + result = char(data[0]) + elif T is uint16 or T is int16: + if data.len != 2: raise newException(ValueError, "invalid 16-bit key") + let bits = (uint16(data[0]) shl 8) or uint16(data[1]) + when T is uint16: result = bits + else: result = cast[int16](bits xor 0x8000'u16) + elif T is uint32 or T is int32: + if data.len != 4: raise newException(ValueError, "invalid 32-bit key") + var bits = 0'u32 + for value in data: bits = (bits shl 8) or uint32(value) + when T is uint32: result = bits + else: result = cast[int32](bits xor 0x80000000'u32) + elif T is uint64 or T is int64: + if data.len != 8: raise newException(ValueError, "invalid 64-bit key") + var bits = 0'u64 + for value in data: bits = (bits shl 8) or uint64(value) + when T is uint64: result = bits + else: result = cast[int64](bits xor 0x8000000000000000'u64) + elif T is int: + when sizeof(int) == 8: result = int(stableKeyDecode[int64](data)) + else: result = int(stableKeyDecode[int32](data)) + elif T is uint: + when sizeof(uint) == 8: result = uint(stableKeyDecode[uint64](data)) + else: result = uint(stableKeyDecode[uint32](data)) + elif T is Principal: + result = Principal.fromBlob(@data) + else: {.error: "IcStableBTreeMap keys require a StableKeyCodec-supported type".} diff --git a/src/nicp_cdk/storage/stable_table.nim b/src/nicp_cdk/storage/stable_table.nim index d0e7e64..60522ad 100644 --- a/src/nicp_cdk/storage/stable_table.nim +++ b/src/nicp_cdk/storage/stable_table.nim @@ -1,177 +1,25 @@ -import std/endians -import std/tables +## Compatibility facade for the stable-memory-native SBT2 B+Tree backend. +## +## An existing STBL v1 region is deliberately rejected by initialization. Use +## `stable_table_migration` to copy it to a separate SBT2 memory view first. -import ./serialization -import ./stable_memory +import ./memory_view +import ./stable_btree -const - TableMagic = [byte('S'), byte('T'), byte('B'), byte('L')] - TableVersion = 1'u32 - TableHeaderSize = 32'u64 +export stable_btree -type EntryInfo = object - offset: uint64 - keyLen: uint32 - valueLen: uint32 +type IcStableTable*[K, V] = IcStableBTreeMap[K, V] -type IcStableTable*[K, V] = object - baseOffset: uint64 - count: uint64 - dataEnd: uint64 - index: Table[string, EntryInfo] +proc initIcStableTable*[K, V](memory: StableMemoryView, cacheSlots: int = 16, + valueCodecId: uint32 = 0): IcStableTable[K, V] = + initIcStableBTreeMap[K, V](memory, cacheSlots, valueCodecId) -proc bytesToString(data: openArray[byte]): string = - result = newString(data.len) - if data.len > 0: - copyMem(addr result[0], unsafeAddr data[0], data.len) +proc initIcStableTable*[K, V](memory: StableMemoryView, codec: StableKeyCodec[K], cacheSlots: int = 16, + valueCodecId: uint32 = 0): IcStableTable[K, V] = + initIcStableBTreeMap[K, V](memory, codec, cacheSlots, valueCodecId) -proc stringToBytes(data: string): seq[byte] = - result = newSeq[byte](data.len) - if data.len > 0: - copyMem(addr result[0], unsafeAddr data[0], data.len) - -proc dataStart(t: IcStableTable): uint64 = - t.baseOffset + TableHeaderSize - -proc writeHeader(t: IcStableTable) = - var header = newSeq[byte](int(TableHeaderSize)) - header[0] = TableMagic[0] - header[1] = TableMagic[1] - header[2] = TableMagic[2] - header[3] = TableMagic[3] - var offset = 4 - var version = TableVersion - littleEndian32(addr header[offset], addr version) - offset += 4 - var count = t.count - littleEndian64(addr header[offset], addr count) - offset += 8 - var dataEnd = t.dataEnd - littleEndian64(addr header[offset], addr dataEnd) - stableWrite(t.baseOffset, header) - -proc readHeader(t: var IcStableTable): bool = - if stableSizeBytes() < t.baseOffset + TableHeaderSize: - return false - let header = stableRead(t.baseOffset, TableHeaderSize) - if header.len < int(TableHeaderSize): - return false - if header[0] != TableMagic[0] or header[1] != TableMagic[1] or - header[2] != TableMagic[2] or header[3] != TableMagic[3]: - return false - var offset = 4 - let version = deserialize[uint32](header, offset) - if version != TableVersion: - return false - t.count = deserialize[uint64](header, offset) - t.dataEnd = deserialize[uint64](header, offset) - let minStart = dataStart(t) - if t.dataEnd < minStart: - t.dataEnd = minStart - let maxEnd = stableSizeBytes() - if t.dataEnd > maxEnd: - t.dataEnd = maxEnd - result = true - -proc rebuildIndex[K, V](t: var IcStableTable[K, V]) = - t.index.clear() - let minStart = dataStart(t) - var offset = minStart - let maxEnd = t.dataEnd - var uniqueCount = 0'u64 - while offset + 8'u64 <= maxEnd: - let lensBytes = stableRead(offset, 8) - var lensOffset = 0 - let keyLen = deserialize[uint32](lensBytes, lensOffset) - let valueLen = deserialize[uint32](lensBytes, lensOffset) - let entrySize = 8'u64 + uint64(keyLen) + uint64(valueLen) - if offset + entrySize > maxEnd: - break - let keyBytes = stableRead(offset + 8'u64, uint64(keyLen)) - let keyStr = bytesToString(keyBytes) - if not t.index.hasKey(keyStr): - uniqueCount += 1 - t.index[keyStr] = EntryInfo(offset: offset, keyLen: keyLen, valueLen: valueLen) - offset += entrySize - t.count = uniqueCount - t.dataEnd = offset - writeHeader(t) - -proc initIcStableTable*[K, V](baseOffset: uint64 = 0): IcStableTable[K, V] = - result.baseOffset = baseOffset - result.index = initTable[string, EntryInfo]() - if not readHeader(result): - result.count = 0 - result.dataEnd = dataStart(result) - writeHeader(result) - rebuildIndex(result) - -proc hasKey*[K, V](t: IcStableTable[K, V], key: K): bool = - let keyBytes = serialize(key) - let keyStr = bytesToString(keyBytes) - result = t.index.hasKey(keyStr) - -proc len*[K, V](t: IcStableTable[K, V]): int = - int(t.count) - -proc `[]`*[K, V](t: var IcStableTable[K, V], key: K): V = - let keyBytes = serialize(key) - let keyStr = bytesToString(keyBytes) - if not t.index.hasKey(keyStr): - raise newException(KeyError, "key not found") - let info = t.index[keyStr] - let valueOffset = info.offset + 8'u64 + uint64(info.keyLen) - let valueBytes = stableRead(valueOffset, uint64(info.valueLen)) - var valuePos = 0 - result = deserialize[V](valueBytes, valuePos) - -proc `[]=`*[K, V](t: var IcStableTable[K, V], key: K, value: V) = - let keyBytes = serialize(key) - let valueBytes = serialize(value) - let keyStr = bytesToString(keyBytes) - if not t.index.hasKey(keyStr): - t.count += 1 - let entryOffset = t.dataEnd - let keyLen = uint32(keyBytes.len) - let valueLen = uint32(valueBytes.len) - var lensBytes = newSeq[byte](8) - var keyLenLe = keyLen - var valueLenLe = valueLen - littleEndian32(addr lensBytes[0], addr keyLenLe) - littleEndian32(addr lensBytes[4], addr valueLenLe) - stableWrite(entryOffset, lensBytes) - stableWrite(entryOffset + 8'u64, keyBytes) - stableWrite(entryOffset + 8'u64 + uint64(keyLen), valueBytes) - t.dataEnd = entryOffset + 8'u64 + uint64(keyLen) + uint64(valueLen) - t.index[keyStr] = EntryInfo(offset: entryOffset, keyLen: keyLen, valueLen: valueLen) - writeHeader(t) - -proc clear*[K, V](t: var IcStableTable[K, V]) = - t.index.clear() - t.count = 0 - t.dataEnd = dataStart(t) - writeHeader(t) - -iterator pairs*[K, V](t: IcStableTable[K, V]): (K, V) = - for keyStr, info in t.index.pairs: - let keyBytes = stringToBytes(keyStr) - var keyPos = 0 - let key = deserialize[K](keyBytes, keyPos) - let valueOffset = info.offset + 8'u64 + uint64(info.keyLen) - let valueBytes = stableRead(valueOffset, uint64(info.valueLen)) - var valuePos = 0 - let value = deserialize[V](valueBytes, valuePos) - yield (key, value) - -iterator keys*[K, V](t: IcStableTable[K, V]): K = - for keyStr, _ in t.index.pairs: - let keyBytes = stringToBytes(keyStr) - var keyPos = 0 - yield deserialize[K](keyBytes, keyPos) - -iterator values*[K, V](t: IcStableTable[K, V]): V = - for _, info in t.index.pairs: - let valueOffset = info.offset + 8'u64 + uint64(info.keyLen) - let valueBytes = stableRead(valueOffset, uint64(info.valueLen)) - var valuePos = 0 - yield deserialize[V](valueBytes, valuePos) +proc initIcStableTable*[K, V](baseOffset: uint64 = 0, limit: uint64 = 0, + cacheSlots: int = 16, valueCodecId: uint32 = 0): IcStableTable[K, V] = + ## `baseOffset` remains for source compatibility. New code should pass a + ## bounded StableMemoryView directly to initIcStableBTreeMap. + initIcStableBTreeMap[K, V](initRawMemoryView(baseOffset, limit), cacheSlots, valueCodecId) diff --git a/src/nicp_cdk/storage/stable_table_migration.nim b/src/nicp_cdk/storage/stable_table_migration.nim new file mode 100644 index 0000000..777ea35 --- /dev/null +++ b/src/nicp_cdk/storage/stable_table_migration.nim @@ -0,0 +1,76 @@ +## Incremental, persistent migration from the append-only STBL v1 format. + +import std/endians +import ./serialization +import ./memory_view +import ./stable_btree + +const + V1Magic = [byte('S'), byte('T'), byte('B'), byte('L')] + MigrationMagic = [byte('S'), byte('M'), byte('I'), byte('2')] + MigrationHeaderSize = 64'u64 + V1HeaderSize = 32'u64 + +type StableTableMigration* = object + source*: StableMemoryView + state*: StableMemoryView + cursor*, dataEnd*: uint64 + completed*: bool + +proc put64(data: var openArray[byte], at: int, value: uint64) = + var x = value; littleEndian64(addr data[at], addr x) +proc get64(data: openArray[byte], at: int): uint64 = + littleEndian64(addr result, unsafeAddr data[at]) + +proc writeState(m: StableTableMigration) = + var data = newSeq[byte](int(MigrationHeaderSize)) + for i in 0 .. 3: data[i] = MigrationMagic[i] + data[4] = byte(if m.completed: 1 else: 0) + data.put64(8, m.cursor); data.put64(16, m.dataEnd) + m.state.write(0, data) + +proc initStableTableMigration*(source, state: StableMemoryView): StableTableMigration = + ## State must be a view dedicated to this migration and must not overlap the + ## v1 source or SBT2 destination view. + result.source = source; result.state = state + if state.size >= MigrationHeaderSize: + let data = state.read(0, MigrationHeaderSize) + var valid = true + for i in 0 .. 3: valid = valid and data[i] == MigrationMagic[i] + if valid: + result.completed = data[4] != 0; result.cursor = data.get64(8); result.dataEnd = data.get64(16) + if result.cursor < V1HeaderSize or result.cursor > result.dataEnd: + raise newException(ValueError, "invalid stable table migration cursor") + return + if source.size < V1HeaderSize: raise newException(ValueError, "STBL v1 header is missing") + let header = source.read(0, V1HeaderSize) + for i in 0 .. 3: + if header[i] != V1Magic[i]: raise newException(ValueError, "STBL v1 magic is missing") + var version: uint32; littleEndian32(addr version, unsafeAddr header[4]) + if version != 1'u32: raise newException(ValueError, "unsupported STBL version") + result.cursor = V1HeaderSize; result.dataEnd = header.get64(16) + if result.dataEnd < result.cursor or result.dataEnd > source.size: + raise newException(ValueError, "invalid STBL v1 data end") + result.writeState + +proc migrateStep*[K, V](m: var StableTableMigration, destination: var IcStableBTreeMap[K, V], + maxRecords: Positive): int = + ## Copies at most `maxRecords` source records. Replaying duplicate keys is + ## intentional: the last v1 append record remains the final SBT2 value. + if m.completed: return 0 + while result < int(maxRecords) and m.cursor < m.dataEnd: + if m.cursor > m.dataEnd - 8'u64: raise newException(ValueError, "truncated STBL v1 record") + let lengths = m.source.read(m.cursor, 8) + var keyLen, valueLen: uint32 + littleEndian32(addr keyLen, unsafeAddr lengths[0]); littleEndian32(addr valueLen, unsafeAddr lengths[4]) + let recordSize = 8'u64 + uint64(keyLen) + uint64(valueLen) + if recordSize > m.dataEnd - m.cursor: raise newException(ValueError, "invalid STBL v1 record length") + let keyData = m.source.read(m.cursor + 8'u64, uint64(keyLen)) + let valueData = m.source.read(m.cursor + 8'u64 + uint64(keyLen), uint64(valueLen)) + var keyPos = 0; var valuePos = 0 + destination[deserialize[K](keyData, keyPos)] = deserialize[V](valueData, valuePos) + m.cursor += recordSize; inc result + if m.cursor == m.dataEnd: m.completed = true + m.writeState + +proc isComplete*(m: StableTableMigration): bool = m.completed diff --git a/tests/storage/test_linear_hashing.nim b/tests/storage/test_linear_hashing.nim new file mode 100644 index 0000000..db5bde6 --- /dev/null +++ b/tests/storage/test_linear_hashing.nim @@ -0,0 +1,24 @@ +discard """cmd: "nim c -r --skipUserCfg $file""" +import std/unittest +import ../../src/nicp_cdk/storage/linear_hashing +suite "linear hashing": + test "one bucket is split at a time": + var state = LinearHashState(level: 1, split: 0) + check state.bucketCount == 2 + check state.splitBucket == 0 + state.advanceSplit; check state.level == 1 and state.split == 1 and state.bucketCount == 3 + state.advanceSplit; check state.level == 2 and state.split == 0 and state.bucketCount == 4 + + test "only an already split bucket uses the next hash bit": + let state = LinearHashState(level: 2, split: 1) + ## 0b100 is bucket 0 before it is split, then bucket 4 afterwards. + check state.bucketIndex(0b000'u64) == 0 + check state.bucketIndex(0b100'u64) == 4 + ## Bucket 1 has not yet been split, so bit 2 is ignored for it. + check state.bucketIndex(0b101'u64) == 1 + + test "invalid persisted state is rejected": + expect ValueError: + discard LinearHashState(level: 2, split: 4).bucketCount + expect ValueError: + discard LinearHashState(level: 33, split: 0).bucketCount diff --git a/tests/storage/test_memory_manager_api.nim b/tests/storage/test_memory_manager_api.nim new file mode 100644 index 0000000..94c23b7 --- /dev/null +++ b/tests/storage/test_memory_manager_api.nim @@ -0,0 +1,17 @@ +discard """ + cmd: "nim check --skipUserCfg $file" +""" + +import ../../src/nicp_cdk/storage/memory_manager +import ../../src/nicp_cdk/storage/stable_btree +import ../../src/nicp_cdk/storage/stable_table + +proc apiShape() = + let manager = initMemoryManager(0) + let users = initVirtualMemory(manager, 1'u8, limit = 1048576'u64) + var tree = initIcStableTable[uint32, string](users.view()) + tree[1'u32] = "one" + discard tree[1'u32] + +static: + doAssert compiles(apiShape()) diff --git a/tests/storage/test_stable_btree.nim b/tests/storage/test_stable_btree.nim new file mode 100644 index 0000000..bbe8ff5 --- /dev/null +++ b/tests/storage/test_stable_btree.nim @@ -0,0 +1,145 @@ +discard """ + cmd: "nim c -r -d:nicpMemoryViewOnly --skipUserCfg $file" +""" + +import std/unittest +import std/options +import std/tables +import std/endians +import ../../src/nicp_cdk/storage/memory_view +import ../../src/nicp_cdk/storage/stable_btree +import ../../src/nicp_cdk/storage/stable_key_codec +import ../../src/nicp_cdk/storage/stable_table_migration +import ../../src/nicp_cdk/storage/serialization + +type InMemoryStable = ref object + data: seq[byte] +type CompositeKey = object + group: uint16 + id: uint16 + +proc compositeCodec(): StableKeyCodec[CompositeKey] = + StableKeyCodec[CompositeKey](id: 1001'u32, + encode: proc(key: CompositeKey): seq[byte] = stableKeyEncode(key.group) & stableKeyEncode(key.id), + decode: proc(data: openArray[byte]): CompositeKey = + if data.len != 4: raise newException(ValueError, "invalid composite key") + CompositeKey(group: stableKeyDecode[uint16](data.toOpenArray(0, 1)), id: stableKeyDecode[uint16](data.toOpenArray(2, 3)))) + +proc memoryView(memory: InMemoryStable): StableMemoryView = + initMemoryView( + proc(): uint64 = uint64(memory.data.len), + proc(offset, size: uint64): seq[byte] = + if offset > uint64(memory.data.len) or size > uint64(memory.data.len) - offset: + raise newException(ValueError, "test memory read out of bounds") + memory.data[int(offset) ..< int(offset + size)], + proc(offset: uint64, data: seq[byte]) = + let endOffset = int(offset) + data.len + if endOffset > memory.data.len: memory.data.setLen(endOffset) + for i, value in data: memory.data[int(offset) + i] = value + ) + +proc put32(data: var openArray[byte], offset: int, value: uint32) = + var le = value + littleEndian32(addr data[offset], addr le) +proc put64(data: var openArray[byte], offset: int, value: uint64) = + var le = value + littleEndian64(addr data[offset], addr le) + +proc appendV1Record(data: var seq[byte], key: uint32, value: string) = + let keyData = serialize(key) + let valueData = serialize(value) + let recordStart = data.len + data.setLen(recordStart + 8 + keyData.len + valueData.len) + data.put32(recordStart, uint32(keyData.len)); data.put32(recordStart + 4, uint32(valueData.len)) + for i, byteValue in keyData: data[recordStart + 8 + i] = byteValue + for i, byteValue in valueData: data[recordStart + 8 + keyData.len + i] = byteValue + +suite "stable B+Tree": + test "split, ordered iteration, update, and reopen": + let backing = InMemoryStable(data: @[]) + var tree = initIcStableBTreeMap[uint32, string](backing.memoryView(), cacheSlots = 0) + for index in countdown(100'u32, 1'u32): tree[index] = "value-" & $index + check tree.len == 100 + for index in 1'u32 .. 100'u32: + check tree.hasKey(index) + check tree[index] == "value-" & $index + var ordered: seq[uint32] = @[] + for key, _ in tree.pairs: ordered.add(key) + check ordered.len == 100 + for index in 0 ..< ordered.len: check ordered[index] == uint32(index + 1) + tree[50'u32] = "updated" + check tree.len == 100 + check tree[50'u32] == "updated" + let bound = tree.lowerBound(50'u32) + check bound.isSome + check bound.get == (50'u32, "updated") + var ranged: seq[uint32] = @[] + for key, _ in tree.range(40'u32, 45'u32): ranged.add(key) + check ranged == @[40'u32, 41'u32, 42'u32, 43'u32, 44'u32] + + var reopened = initIcStableBTreeMap[uint32, string](backing.memoryView()) + check reopened.len == 100 + check reopened[50'u32] == "updated" + check reopened[100'u32] == "value-100" + + test "deterministic random upserts match a heap reference": + let backing = InMemoryStable(data: @[]) + var tree = initIcStableBTreeMap[uint32, uint64](backing.memoryView()) + var reference = initTable[uint32, uint64]() + var state = 0x9E3779B97F4A7C15'u64 + for _ in 0 ..< 1000: + state = state xor (state shl 7) + state = state xor (state shr 9) + let key = uint32(state mod 200'u64) + let value = state xor (state shr 17) + tree[key] = value + reference[key] = value + check tree.len == reference.len + for key, value in reference.pairs: + check tree.hasKey(key) + check tree[key] == value + var previous = none(uint32) + for key, value in tree.pairs: + if previous.isSome: check previous.get < key + check reference[key] == value + previous = some(key) + + test "v1 migration resumes and keeps the last duplicate value": + let source = InMemoryStable(data: newSeq[byte](32)) + source.data[0] = byte('S'); source.data[1] = byte('T'); source.data[2] = byte('B'); source.data[3] = byte('L') + source.data.put32(4, 1'u32) + source.data.appendV1Record(1'u32, "first") + source.data.appendV1Record(2'u32, "second") + source.data.appendV1Record(1'u32, "latest") + source.data.put64(16, uint64(source.data.len)) + let migrationState = InMemoryStable(data: @[]) + let destinationBacking = InMemoryStable(data: @[]) + var destination = initIcStableBTreeMap[uint32, string](destinationBacking.memoryView()) + var migration = initStableTableMigration(source.memoryView(), migrationState.memoryView()) + check migration.migrateStep(destination, 1) == 1 + check not migration.isComplete + var resumed = initStableTableMigration(source.memoryView(), migrationState.memoryView()) + check resumed.migrateStep(destination, 10) == 2 + check resumed.isComplete + check destination.len == 2 + check destination[1'u32] == "latest" + check destination[2'u32] == "second" + + test "custom key codec is persisted and used for ordering": + let backing = InMemoryStable(data: @[]) + let codec = compositeCodec() + var tree = initIcStableBTreeMap[CompositeKey, string](backing.memoryView(), codec) + tree[CompositeKey(group: 2, id: 1)] = "two-one" + tree[CompositeKey(group: 1, id: 9)] = "one-nine" + var keys: seq[CompositeKey] = @[] + for key, _ in tree.pairs: keys.add(key) + check keys == @[CompositeKey(group: 1, id: 9), CompositeKey(group: 2, id: 1)] + var reopened = initIcStableBTreeMap[CompositeKey, string](backing.memoryView(), codec) + check reopened[CompositeKey(group: 2, id: 1)] == "two-one" + + test "value codec mismatch is rejected on reopen": + let backing = InMemoryStable(data: @[]) + var tree = initIcStableBTreeMap[uint32, string](backing.memoryView(), valueCodecId = 1) + tree[1'u32] = "one" + expect ValueError: + discard initIcStableBTreeMap[uint32, string](backing.memoryView(), valueCodecId = 2) diff --git a/tests/storage/test_stable_btree_api.nim b/tests/storage/test_stable_btree_api.nim new file mode 100644 index 0000000..e881f4b --- /dev/null +++ b/tests/storage/test_stable_btree_api.nim @@ -0,0 +1,39 @@ +discard """ + cmd: "nim check --skipUserCfg $file" +""" + +## Compile-time coverage for the public generic API. Runtime/reopen coverage +## runs in the canister test environment because stable64 is an ic0 import. +import ../../src/nicp_cdk/storage/stable_btree +import ../../src/nicp_cdk/storage/stable_table +import ../../src/nicp_cdk/storage/stable_table_migration +import ../../src/nicp_cdk/storage/memory_view + +proc apiShape() = + var table = initIcStableBTreeMap[string, uint64]() + table["one"] = 1 + discard table.hasKey("one") + discard table["one"] + discard table.len + discard table.lowerBound("one") + for key, value in table.pairs: + discard key + discard value + for key, value in table.range("a", "z"): + discard key + discard value + table.clear() + var uncached = initIcStableBTreeMap[uint32, string](cacheSlots = 0) + uncached[1'u32] = "one" + discard uncached[1'u32] + +proc facadeAndMigrationShape() = + var table = initIcStableTable[string, uint64](1024, limit = 65536) + table["one"] = 1 + var migration = initStableTableMigration(initRawMemoryView(0, 1024), initRawMemoryView(2048, 64)) + discard migration.migrateStep(table, 1) + discard migration.isComplete + +static: + doAssert compiles(apiShape()) + doAssert compiles(facadeAndMigrationShape()) diff --git a/tests/storage/test_stable_hash.nim b/tests/storage/test_stable_hash.nim new file mode 100644 index 0000000..0e86fb1 --- /dev/null +++ b/tests/storage/test_stable_hash.nim @@ -0,0 +1,14 @@ +discard """cmd: "nim c -r --skipUserCfg $file""" +import std/unittest +import ../../src/nicp_cdk/storage/stable_hash +suite "stable hash": + test "matches the SipHash-2-4 reference vector": + let seed = StableHashSeed(k0: 0x0706050403020100'u64, + k1: 0x0f0e0d0c0b0a0908'u64) + check sipHash24(seed, @[]) == 0x726fdb47dd0e0e31'u64 + check sipHash24(seed, @[0'u8]) == 0x74f839c593dc67fd'u64 + + test "seeded and deterministic": + let data = @[byte('k'), byte('e'), byte('y')] + check sipHash24(StableHashSeed(k0: 1, k1: 2), data) == sipHash24(StableHashSeed(k0: 1, k1: 2), data) + check sipHash24(StableHashSeed(k0: 1, k1: 2), data) != sipHash24(StableHashSeed(k0: 2, k1: 1), data) diff --git a/tests/storage/test_stable_hash_map.nim b/tests/storage/test_stable_hash_map.nim new file mode 100644 index 0000000..6c48973 --- /dev/null +++ b/tests/storage/test_stable_hash_map.nim @@ -0,0 +1,57 @@ +discard """ + cmd: "nim c -r -d:nicpMemoryViewOnly --skipUserCfg $file" +""" + +import std/unittest +import std/tables +import ../../src/nicp_cdk/storage/memory_view +import ../../src/nicp_cdk/storage/stable_hash_map + +type InMemoryStable = ref object + data: seq[byte] + +proc memoryView(memory: InMemoryStable): StableMemoryView = + initMemoryView( + proc(): uint64 = uint64(memory.data.len), + proc(offset, size: uint64): seq[byte] = + if offset > uint64(memory.data.len) or size > uint64(memory.data.len) - offset: + raise newException(ValueError, "test memory read out of bounds") + memory.data[int(offset) ..< int(offset + size)], + proc(offset: uint64, data: seq[byte]) = + let endOffset = int(offset) + data.len + if endOffset > memory.data.len: memory.data.setLen(endOffset) + for i, value in data: memory.data[int(offset) + i] = value + ) + +suite "stable hash map": + test "incremental splits, updates, iteration, and reopen": + let backing = InMemoryStable(data: @[]) + var table = initIcStableHashMap[uint32, string](backing.memoryView(), maxBucketLoad = 2) + var expected = initTable[uint32, string]() + for key in 0'u32 ..< 100'u32: + let value = "value-" & $key + table[key] = value + expected[key] = value + table[17'u32] = "updated" + expected[17'u32] = "updated" + check table.len == expected.len + for key, value in expected.pairs: + check table.hasKey(key) + check table[key] == value + var iterated = initTable[uint32, string]() + for key, value in table.pairs: iterated[key] = value + check iterated == expected + + var reopened = initIcStableHashMap[uint32, string](backing.memoryView(), maxBucketLoad = 2) + check reopened.len == expected.len + for key, value in expected.pairs: check reopened[key] == value + + test "clear reuses the logical arena": + let backing = InMemoryStable(data: @[]) + var table = initIcStableHashMap[string, uint64](backing.memoryView()) + table["old"] = 1 + table.clear() + check table.len == 0 + check not table.hasKey("old") + table["new"] = 2 + check table["new"] == 2 diff --git a/tests/storage/test_stable_key_codec.nim b/tests/storage/test_stable_key_codec.nim new file mode 100644 index 0000000..8deba60 --- /dev/null +++ b/tests/storage/test_stable_key_codec.nim @@ -0,0 +1,29 @@ +discard """ + cmd: "nim c -r --skipUserCfg $file" +""" + +import std/unittest +import ../../src/nicp_cdk/storage/stable_key_codec + +proc compareBytes(a, b: openArray[byte]): int = + for i in 0 ..< min(a.len, b.len): + if a[i] != b[i]: return if a[i] < b[i]: -1 else: 1 + system.cmp(a.len, b.len) + +suite "stable key codec": + test "signed integer encoding preserves numeric order": + let values = @[-32768'i16, -2'i16, -1'i16, 0'i16, 1'i16, 255'i16, 32767'i16] + for i in 0 ..< values.high: + check compareBytes(stableKeyEncode(values[i]), stableKeyEncode(values[i + 1])) < 0 + + test "unsigned integer encoding is big endian and reversible": + let values = @[0'u32, 1'u32, 255'u32, 256'u32, high(uint32)] + for i, value in values: + check stableKeyDecode[uint32](stableKeyEncode(value)) == value + if i > 0: + check compareBytes(stableKeyEncode(values[i - 1]), stableKeyEncode(value)) < 0 + + test "strings have no length prefix in their ordering key": + check stableKeyEncode("z") == @[byte('z')] + check compareBytes(stableKeyEncode("a-long-key"), stableKeyEncode("b")) < 0 + check stableKeyDecode[string](stableKeyEncode("日本語")) == "日本語" diff --git a/tests/types/test_nat.nim b/tests/types/test_nat.nim index b2df938..3fac679 100644 --- a/tests/types/test_nat.nim +++ b/tests/types/test_nat.nim @@ -44,6 +44,12 @@ suite "ic_nat tests": let request = newMockRequest(decoded.values) check request.getNat32(0) == n + test("nat32-compatible key accepts nat and nat32"): + let natRequest = newMockRequest(@[newCandidNat(10'u)]) + let nat32Request = newMockRequest(@[newCandidNat32(10'u32)]) + check natRequest.getNat32Compatible(0) == 10'u32 + check nat32Request.getNat32Compatible(0) == 10'u32 + test("nat64"): let n = 10.uint64 let candidNat64 = newCandidNat64(n) From 67763c8009dfa2c4097e0e09de126660ab4803f1 Mon Sep 17 00:00:00 2001 From: itsumura-h Date: Sun, 23 Aug 2026 17:50:53 +0000 Subject: [PATCH 02/13] add test --- tests/storage/test_stable_memory.nim | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/storage/test_stable_memory.nim b/tests/storage/test_stable_memory.nim index ed21af3..1655c2f 100644 --- a/tests/storage/test_stable_memory.nim +++ b/tests/storage/test_stable_memory.nim @@ -86,6 +86,7 @@ proc resetAllDatabases() = discard callCanisterFunction("byte_set", "(0)") discard callCanisterFunction("seqInt_reset") discard callCanisterFunction("table_reset") + discard callCanisterFunction("btree_reset") suite "stable memory backend tests": deploy() @@ -198,6 +199,29 @@ suite "stable memory backend tests": check values.contains("\"root2\"") check values.contains("\"anon\"") + test "IcStableBTreeMap[string, string]": + discard callCanisterFunction("btree_reset") + check callCanisterFunction("btree_len") == "(0 : nat)" + check callCanisterFunction("btree_hasKey", "(\"one\")") == "(false)" + + discard callCanisterFunction("btree_set", "(\"two\", \"second\")") + discard callCanisterFunction("btree_set", "(\"one\", \"first\")") + check callCanisterFunction("btree_len") == "(2 : nat)" + check callCanisterFunction("btree_get", "(\"one\")") == "(\"first\")" + + ## Updating an existing key must not change the live-entry count. + discard callCanisterFunction("btree_set", "(\"one\", \"updated\")") + check callCanisterFunction("btree_len") == "(2 : nat)" + check callCanisterFunction("btree_get", "(\"one\")") == "(\"updated\")" + check callCanisterFunction("btree_hasKey", "(\"two\")") == "(true)" + + let ranged = callCanisterFunction("btree_range", "(\"a\", \"z\")") + check ranged.contains("key = \"one\"") + check ranged.contains("value = \"updated\"") + check ranged.contains("key = \"two\"") + check ranged.contains("value = \"second\"") + check ranged.find("key = \"one\"") < ranged.find("key = \"two\"") + test "object": discard callCanisterFunction("object_set", "(1, \"Alice\", true)") var value = callCanisterFunction("object_get") From 6ec0c5a311465b9c69cf9e386a0eb416c25b1ca8 Mon Sep 17 00:00:00 2001 From: itsumura-h Date: Sun, 23 Aug 2026 18:13:52 +0000 Subject: [PATCH 03/13] rm IcStableTable --- .cursor/rules/branch/118-icstablebtree.mdc | 1 + README.md | 18 +-- docs/en/stable_memory.md | 29 +++-- examples/stable_memory/backend/backend.did | 11 -- examples/stable_memory/backend/src/main.nim | 87 +------------- .../src/stable_memory_backend/main.nim | 112 +++++++----------- examples/stable_memory/stable_memory.did | 17 +-- examples/stable_memory/test_clean.sh | 14 +-- examples/stable_memory/test_table_seqint.sh | 35 ------ src/nicp_cdk/storage/stable_table.nim | 25 ---- .../storage/stable_table_migration.nim | 76 ------------ tests/storage/test_memory_manager_api.nim | 3 +- tests/storage/test_stable_btree.nim | 40 ------- tests/storage/test_stable_btree_api.nim | 10 -- tests/storage/test_stable_memory.nim | 51 +------- 15 files changed, 82 insertions(+), 447 deletions(-) delete mode 100755 examples/stable_memory/test_table_seqint.sh delete mode 100644 src/nicp_cdk/storage/stable_table.nim delete mode 100644 src/nicp_cdk/storage/stable_table_migration.nim diff --git a/.cursor/rules/branch/118-icstablebtree.mdc b/.cursor/rules/branch/118-icstablebtree.mdc index 2b6c3bf..1a5362e 100644 --- a/.cursor/rules/branch/118-icstablebtree.mdc +++ b/.cursor/rules/branch/118-icstablebtree.mdc @@ -34,6 +34,7 @@ Draft 0.1 \| 2026-08-20 - [x] Phase 5(基盤): versioned SipHash-2-4 と persisted linear-hashing state transition を追加し、reference vector と split routing をテストした。 - [x] Phase 5: `IcStableHashMap` の persistent bucket/overflow-chain 実装を追加。線形分割は1バケットだけを再配置し、全件 rehash を行わない。 - [x] Phase 5: exact-match workload benchmark を `benchmarks/storage/stable_exact_lookup.nim` に追加。in-memory backend・cacheSlots=0 の結果では、1k entries は HashMap が優位(1 ms / 1.20 MB vs 4 ms / 3.12 MB)だが、10k entries は B+Tree が優位(54 ms / 41.62 MB vs 63 ms / 57.60 MB)だった。順序 API と大規模時の read byte を考慮し、`IcStableTable` の既定は B+Tree のままとする。 +- [x] `IcStableTable` の互換 facade・旧 STBL migration・サンプルの `table_*`/`object_*` API を削除し、公開・サンプル・統合テストを `IcStableBTreeMap` に統一した。 本設計の目的は、NICP の stable memory 上に「検索可能な index 自体」を永続化し、canister 起動・アップグレード後に全件を heap へ復元しなくても利用できる key-value storage を実装することである。対象は主に現行 IcStableTable の置き換えであり、既存の Nim らしい API は可能な限り維持する。 diff --git a/README.md b/README.md index 2ba25fb..82d895e 100644 --- a/README.md +++ b/README.md @@ -195,15 +195,15 @@ seqIntDb.clear() Supported element types: primitive types and Principal -### IcStableTable - Persistent Key-Value Store +### IcStableBTreeMap - Persistent Key-Value Store Store key-value pairs that persist across canister upgrades. ```nim -import nicp_cdk/storage/stable_table +import nicp_cdk/storage/stable_btree -# Create a stable table mapping strings to integers -var scoreTable = initIcStableTable[string, uint]() +# Create a stable B+Tree mapping strings to integers +var scoreTable = initIcStableBTreeMap[string, uint]() # Store a key-value pair scoreTable["alice"] = 100 @@ -216,7 +216,7 @@ let aliceScore = scoreTable["alice"] if scoreTable.hasKey("alice"): echo "Alice has a score" -# Get table size +# Get map size let numPlayers = scoreTable.len() # Iterate over all pairs @@ -232,19 +232,19 @@ Supported value types: primitive types, Principal, and Nim objects ### Example: Storing Custom Objects -You can also store custom Nim objects in a stable table: +You can also store custom Nim objects in a stable B+Tree: ```nim import nicp_cdk -import nicp_cdk/storage/stable_table +import nicp_cdk/storage/stable_btree type UserProfile = object id: uint name: string active: bool -# Create a stable table mapping principals to user profiles -var userTable = initIcStableTable[Principal, UserProfile]() +# Create a stable B+Tree mapping principals to user profiles +var userTable = initIcStableBTreeMap[Principal, UserProfile]() # Store a user profile let caller = Msg.caller() diff --git a/docs/en/stable_memory.md b/docs/en/stable_memory.md index f18149e..086f073 100644 --- a/docs/en/stable_memory.md +++ b/docs/en/stable_memory.md @@ -11,7 +11,7 @@ types built on top of stable memory: - `IcStableValue[T]` for a single value - `IcStableSeq[T]` for a sequence of values -- `IcStableTable[K, V]` for a key-value store +- `IcStableBTreeMap[K, V]` for an ordered key-value store All values are serialized with the custom format in `src/nicp_cdk/storage/serialization.nim`. @@ -41,12 +41,12 @@ items.delete(0) let length = items.len() ``` -### IcStableTable +### IcStableBTreeMap ```nim -import nicp_cdk/storage/stable_table +import nicp_cdk/storage/stable_btree -var table = initIcStableTable[string, uint64]() +var table = initIcStableBTreeMap[string, uint64]() table["alice"] = 100 if table.hasKey("alice"): echo table["alice"] @@ -82,26 +82,25 @@ Header size: 32 bytes. 32.. entries: [elemLen u32][elemBytes] ... ``` -### IcStableTable layout +### IcStableBTreeMap layout -Header size: 32 bytes. +The B+Tree persists an `SBT2` superblock, node pages, and key/value blobs. +Initialization reads only the superblock and bounded cache metadata; it does +not rebuild an in-memory index by scanning all entries. ``` -0..3 magic "STBL" -4..7 version (u32, little-endian) -8..15 element count (u64, little-endian) -16..23 data end offset (u64, little-endian) -24..31 reserved -32.. entries: [keyLen u32][valueLen u32][keyBytes][valueBytes] ... +0..3 magic "SBT2" +4.. versioned superblock, root address, count, allocator metadata +... fixed-size B+Tree node pages and variable key/value blobs ``` -Entries are append-only. On initialization, the table or sequence scans the data -area to rebuild its in-memory index. +Entries are searched directly in stable memory. Keys are stored with an +order-preserving codec, allowing ordered iteration and range queries. ## Serialization Notes - Fixed-size values are stored in little-endian byte order. -- Variable-size values (string, Principal, seq, Table) are stored as +- Variable-size values (string, Principal, seq, B+Tree values) are stored as `length (u32) + bytes`. - Nim objects are serialized by field order. diff --git a/examples/stable_memory/backend/backend.did b/examples/stable_memory/backend/backend.did index 1d33b99..2da2349 100644 --- a/examples/stable_memory/backend/backend.did +++ b/examples/stable_memory/backend/backend.did @@ -24,17 +24,6 @@ service : { "seqInt_setAt": (nat, int) -> (); "seqInt_delete": (nat) -> (); "seqInt_values": () -> (vec int) query; - "table_reset": () -> (); - "table_set": (text) -> (); - "table_get": () -> (text) query; - "table_len": () -> (nat) query; - "table_hasKey": () -> (bool) query; - "table_setFor": (principal, text) -> (); - "table_getFor": (principal) -> (text) query; - "table_keys": () -> (vec principal) query; - "table_values": () -> (vec text) query; - "object_set": (id : nat, name : text, active : bool) -> (); - "object_get": () -> (record { id : nat; name : text; active : bool }) query; "btree_reset": () -> (); "btree_set": (text, text) -> (); "btree_get": (text) -> (text) query; diff --git a/examples/stable_memory/backend/src/main.nim b/examples/stable_memory/backend/src/main.nim index 2b9f5db..f11f423 100644 --- a/examples/stable_memory/backend/src/main.nim +++ b/examples/stable_memory/backend/src/main.nim @@ -1,8 +1,6 @@ -import std/tables import ../../../../src/nicp_cdk import ../../../../src/nicp_cdk/storage/stable_value import ../../../../src/nicp_cdk/storage/stable_seq -import ../../../../src/nicp_cdk/storage/stable_table import ../../../../src/nicp_cdk/storage/stable_btree import ../../../../src/nicp_cdk/storage/memory_view @@ -18,9 +16,7 @@ const CharDbOffset = 700000'u64 ByteDbOffset = 800000'u64 SeqIntDbOffset = 900000'u64 - TableDbOffset = 2000000'u64 - ObjectDbOffset = 3000000'u64 - BTreeDbOffset = 4000000'u64 + BTreeDbOffset = 2000000'u64 BTreeDbLimit = 1000000'u64 # ================================================== @@ -205,87 +201,6 @@ proc seqInt_delete() {.update.} = proc seqInt_values() {.query.} = reply(seqIntDb.toSeq()) -# ================================================== -# Table[principal, string] -# ================================================== -var tableDb = initIcStableTable[Principal, string](TableDbOffset, limit = 1000000'u64) - -proc table_reset() {.update.} = - tableDb.clear() - reply() - -proc table_set() {.update.} = - let principal = Msg.caller() - let request = Request.new() - let message = request.getStr(0) - tableDb[principal] = message - reply() - -proc table_get() {.query.} = - let principal = Msg.caller() - let value = tableDb[principal] - reply(value) - -proc table_len() {.query.} = - reply(uint(tableDb.len())) - -proc table_hasKey() {.query.} = - let principal = Msg.caller() - reply(tableDb.hasKey(principal)) - -proc table_setFor() {.update.} = - let request = Request.new() - let principal = request.getPrincipal(0) - let message = request.getStr(1) - tableDb[principal] = message - reply() - -proc table_getFor() {.query.} = - let request = Request.new() - let principal = request.getPrincipal(0) - let value = tableDb[principal] - reply(value) - -proc table_keys() {.query.} = - var keys: seq[Principal] = @[] - for key in tableDb.keys(): - keys.add(key) - reply(keys) - -proc table_values() {.query.} = - var values: seq[string] = @[] - for value in tableDb.values(): - values.add(value) - reply(values) - -# ================================================== -# object -# ================================================== -type UserProfile = object - id: uint - name: string - active: bool - -var objectDb = initIcStableTable[Principal, UserProfile](ObjectDbOffset, limit = 1000000'u64) - -proc object_set() {.update.} = - try: - let principal = Msg.caller() - let request = Request.new() - let id = request.getNat(0) - let name = request.getStr(1) - let active = request.getBool(2) - objectDb[principal] = UserProfile(id: id, name: name, active: active) - reply() - except Exception as e: - echo "Error: ", e.msg - reply(e.msg) - -proc object_get() {.query.} = - let principal = Msg.caller() - let value = objectDb[principal] - reply(value) - # ================================================== # IcStableBTreeMap[string, string] # ================================================== diff --git a/examples/stable_memory/src/stable_memory_backend/main.nim b/examples/stable_memory/src/stable_memory_backend/main.nim index 6583483..7a15e93 100644 --- a/examples/stable_memory/src/stable_memory_backend/main.nim +++ b/examples/stable_memory/src/stable_memory_backend/main.nim @@ -1,8 +1,8 @@ -import std/tables import ../../../../src/nicp_cdk import ../../../../src/nicp_cdk/storage/stable_value import ../../../../src/nicp_cdk/storage/stable_seq -import ../../../../src/nicp_cdk/storage/stable_table +import ../../../../src/nicp_cdk/storage/stable_btree +import ../../../../src/nicp_cdk/storage/memory_view # Define base offsets for each storage structure to avoid collision const @@ -16,8 +16,8 @@ const CharDbOffset = 700000'u64 ByteDbOffset = 800000'u64 SeqIntDbOffset = 900000'u64 - TableDbOffset = 2000000'u64 - ObjectDbOffset = 3000000'u64 + BTreeDbOffset = 2000000'u64 + BTreeDbLimit = 1000000'u64 # ================================================== # int @@ -202,82 +202,50 @@ proc seqInt_values() {.query.} = reply(seqIntDb.toSeq()) # ================================================== -# Table[principal, string] +# IcStableBTreeMap[string, string] # ================================================== -var tableDb = initIcStableTable[Principal, string](TableDbOffset, limit = 1000000'u64) +type BTreeEntry = object + key: string + value: string -proc table_reset() {.update.} = - tableDb.clear() - reply() +var btreeDb = initIcStableBTreeMap[string, string]( + initRawMemoryView(BTreeDbOffset, BTreeDbLimit) +) -proc table_set() {.update.} = - let principal = Msg.caller() - let request = Request.new() - let message = request.getStr(0) - tableDb[principal] = message +proc btree_reset() {.update.} = + btreeDb.clear() reply() -proc table_get() {.query.} = - let principal = Msg.caller() - let value = tableDb[principal] - reply(value) - -proc table_len() {.query.} = - reply(uint(tableDb.len())) - -proc table_hasKey() {.query.} = - let principal = Msg.caller() - reply(tableDb.hasKey(principal)) +proc btree_set() {.update.} = + try: + icEcho("btree_set: begin") + let request = Request.new() + let key = request.getStr(0) + let value = request.getStr(1) + icEcho("btree_set: writing key=", key) + btreeDb[key] = value + icEcho("btree_set: write complete") + reply() + except Exception as e: + icEcho("btree_set failed: ", e.msg) + raise -proc table_setFor() {.update.} = +proc btree_get() {.query.} = let request = Request.new() - let principal = request.getPrincipal(0) - let message = request.getStr(1) - tableDb[principal] = message - reply() + reply(btreeDb[request.getStr(0)]) -proc table_getFor() {.query.} = +proc btree_hasKey() {.query.} = let request = Request.new() - let principal = request.getPrincipal(0) - let value = tableDb[principal] - reply(value) - -proc table_keys() {.query.} = - var keys: seq[Principal] = @[] - for key in tableDb.keys(): - keys.add(key) - reply(keys) + reply(btreeDb.hasKey(request.getStr(0))) -proc table_values() {.query.} = - var values: seq[string] = @[] - for value in tableDb.values(): - values.add(value) - reply(values) +proc btree_len() {.query.} = + reply(uint(btreeDb.len())) -# ================================================== -# object -# ================================================== -type UserProfile = object - id: uint - name: string - active: bool - -var objectDb = initIcStableTable[Principal, UserProfile](ObjectDbOffset, limit = 1000000'u64) - -proc object_set() {.update.} = - try: - let principal = Msg.caller() - let request = Request.new() - let id = request.getNat(0) - let name = request.getStr(1) - let active = request.getBool(2) - objectDb[principal] = UserProfile(id: id, name: name, active: active) - reply() - except Exception as e: - echo "Error: ", e.msg - reply(e.msg) - -proc object_get() {.query.} = - let principal = Msg.caller() - let value = objectDb[principal] - reply(value) +proc btree_range() {.query.} = + let request = Request.new() + let startKey = request.getStr(0) + let endKey = request.getStr(1) + var entries: seq[BTreeEntry] = @[] + for key, value in btreeDb.range(startKey, endKey): + entries.add(BTreeEntry(key: key, value: value)) + reply(entries) diff --git a/examples/stable_memory/stable_memory.did b/examples/stable_memory/stable_memory.did index 5806db8..2da2349 100644 --- a/examples/stable_memory/stable_memory.did +++ b/examples/stable_memory/stable_memory.did @@ -24,15 +24,10 @@ service : { "seqInt_setAt": (nat, int) -> (); "seqInt_delete": (nat) -> (); "seqInt_values": () -> (vec int) query; - "table_reset": () -> (); - "table_set": (text) -> (); - "table_get": () -> (text) query; - "table_len": () -> (nat) query; - "table_hasKey": () -> (bool) query; - "table_setFor": (principal, text) -> (); - "table_getFor": (principal) -> (text) query; - "table_keys": () -> (vec principal) query; - "table_values": () -> (vec text) query; - "object_set": (id : nat, name : text, active : bool) -> (); - "object_get": () -> (record { id : nat; name : text; active : bool }) query; + "btree_reset": () -> (); + "btree_set": (text, text) -> (); + "btree_get": (text) -> (text) query; + "btree_hasKey": (text) -> (bool) query; + "btree_len": () -> (nat) query; + "btree_range": (text, text) -> (vec record { key : text; value : text }) query; }; diff --git a/examples/stable_memory/test_clean.sh b/examples/stable_memory/test_clean.sh index f5ee99a..b592d9c 100755 --- a/examples/stable_memory/test_clean.sh +++ b/examples/stable_memory/test_clean.sh @@ -11,12 +11,12 @@ $DFX canister call stable_memory_backend double_set '(0.0 : float64)' $DFX canister call stable_memory_backend char_set '(0)' $DFX canister call stable_memory_backend byte_set '(0)' $DFX canister call stable_memory_backend seqInt_reset '()' -$DFX canister call stable_memory_backend table_reset '()' +$DFX canister call stable_memory_backend btree_reset '()' echo "" echo "=== Test upgrade preserves stable memory scenario ===" $DFX canister call stable_memory_backend seqInt_reset '()' -$DFX canister call stable_memory_backend table_reset '()' +$DFX canister call stable_memory_backend btree_reset '()' echo "Setting int to 123" $DFX canister call stable_memory_backend int_set '(123)' @@ -28,7 +28,7 @@ echo "Adding 8 to seqInt" $DFX canister call stable_memory_backend seqInt_set '(8)' echo "Setting principal -> 'upgrade' in table" -$DFX canister call stable_memory_backend table_setFor "(principal \"aaaaa-aa\", \"upgrade\")" +$DFX canister call stable_memory_backend btree_set '("upgrade", "upgrade")' echo "" echo "=== Before upgrade ===" @@ -40,8 +40,8 @@ echo "seqInt_get(0):" $DFX canister call stable_memory_backend seqInt_get '(0)' echo "seqInt_get(1):" $DFX canister call stable_memory_backend seqInt_get '(1)' -echo "table_getFor(aaaaa-aa):" -$DFX canister call stable_memory_backend table_getFor "(principal \"aaaaa-aa\")" +echo "btree_get(upgrade):" +$DFX canister call stable_memory_backend btree_get '("upgrade")' echo "" echo "=== Upgrading ===" @@ -57,5 +57,5 @@ echo "seqInt_get(0):" $DFX canister call stable_memory_backend seqInt_get '(0)' echo "seqInt_get(1):" $DFX canister call stable_memory_backend seqInt_get '(1)' -echo "table_getFor(aaaaa-aa):" -$DFX canister call stable_memory_backend table_getFor "(principal \"aaaaa-aa\")" +echo "btree_get(upgrade):" +$DFX canister call stable_memory_backend btree_get '("upgrade")' diff --git a/examples/stable_memory/test_table_seqint.sh b/examples/stable_memory/test_table_seqint.sh deleted file mode 100755 index 5380f33..0000000 --- a/examples/stable_memory/test_table_seqint.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash -DFX="/root/.local/share/dfx/bin/dfx" - -echo "=== Deploy ===" -$DFX deploy stable_memory_backend -y 2>&1 | tail -3 - -echo "" -echo "=== Initial seqInt operations ===" -$DFX canister call stable_memory_backend seqInt_reset '()' -$DFX canister call stable_memory_backend seqInt_set '(7)' -$DFX canister call stable_memory_backend seqInt_set '(8)' -echo "seqInt_get(0):" -$DFX canister call stable_memory_backend seqInt_get '(0)' -echo "seqInt_get(1):" -$DFX canister call stable_memory_backend seqInt_get '(1)' - -echo "" -echo "=== Before table_setFor ===" -echo "seqInt_get(0):" -$DFX canister call stable_memory_backend seqInt_get '(0)' - -echo "" -echo "=== Call table_setFor ===" -$DFX canister call stable_memory_backend table_setFor "(principal \"aaaaa-aa\", \"upgrade\")" - -echo "" -echo "=== After table_setFor ===" -echo "seqInt_get(0):" -$DFX canister call stable_memory_backend seqInt_get '(0)' -echo "seqInt_get(1):" -$DFX canister call stable_memory_backend seqInt_get '(1)' - -echo "" -echo "=== table_getFor ===" -$DFX canister call stable_memory_backend table_getFor "(principal \"aaaaa-aa\")" diff --git a/src/nicp_cdk/storage/stable_table.nim b/src/nicp_cdk/storage/stable_table.nim deleted file mode 100644 index 60522ad..0000000 --- a/src/nicp_cdk/storage/stable_table.nim +++ /dev/null @@ -1,25 +0,0 @@ -## Compatibility facade for the stable-memory-native SBT2 B+Tree backend. -## -## An existing STBL v1 region is deliberately rejected by initialization. Use -## `stable_table_migration` to copy it to a separate SBT2 memory view first. - -import ./memory_view -import ./stable_btree - -export stable_btree - -type IcStableTable*[K, V] = IcStableBTreeMap[K, V] - -proc initIcStableTable*[K, V](memory: StableMemoryView, cacheSlots: int = 16, - valueCodecId: uint32 = 0): IcStableTable[K, V] = - initIcStableBTreeMap[K, V](memory, cacheSlots, valueCodecId) - -proc initIcStableTable*[K, V](memory: StableMemoryView, codec: StableKeyCodec[K], cacheSlots: int = 16, - valueCodecId: uint32 = 0): IcStableTable[K, V] = - initIcStableBTreeMap[K, V](memory, codec, cacheSlots, valueCodecId) - -proc initIcStableTable*[K, V](baseOffset: uint64 = 0, limit: uint64 = 0, - cacheSlots: int = 16, valueCodecId: uint32 = 0): IcStableTable[K, V] = - ## `baseOffset` remains for source compatibility. New code should pass a - ## bounded StableMemoryView directly to initIcStableBTreeMap. - initIcStableBTreeMap[K, V](initRawMemoryView(baseOffset, limit), cacheSlots, valueCodecId) diff --git a/src/nicp_cdk/storage/stable_table_migration.nim b/src/nicp_cdk/storage/stable_table_migration.nim deleted file mode 100644 index 777ea35..0000000 --- a/src/nicp_cdk/storage/stable_table_migration.nim +++ /dev/null @@ -1,76 +0,0 @@ -## Incremental, persistent migration from the append-only STBL v1 format. - -import std/endians -import ./serialization -import ./memory_view -import ./stable_btree - -const - V1Magic = [byte('S'), byte('T'), byte('B'), byte('L')] - MigrationMagic = [byte('S'), byte('M'), byte('I'), byte('2')] - MigrationHeaderSize = 64'u64 - V1HeaderSize = 32'u64 - -type StableTableMigration* = object - source*: StableMemoryView - state*: StableMemoryView - cursor*, dataEnd*: uint64 - completed*: bool - -proc put64(data: var openArray[byte], at: int, value: uint64) = - var x = value; littleEndian64(addr data[at], addr x) -proc get64(data: openArray[byte], at: int): uint64 = - littleEndian64(addr result, unsafeAddr data[at]) - -proc writeState(m: StableTableMigration) = - var data = newSeq[byte](int(MigrationHeaderSize)) - for i in 0 .. 3: data[i] = MigrationMagic[i] - data[4] = byte(if m.completed: 1 else: 0) - data.put64(8, m.cursor); data.put64(16, m.dataEnd) - m.state.write(0, data) - -proc initStableTableMigration*(source, state: StableMemoryView): StableTableMigration = - ## State must be a view dedicated to this migration and must not overlap the - ## v1 source or SBT2 destination view. - result.source = source; result.state = state - if state.size >= MigrationHeaderSize: - let data = state.read(0, MigrationHeaderSize) - var valid = true - for i in 0 .. 3: valid = valid and data[i] == MigrationMagic[i] - if valid: - result.completed = data[4] != 0; result.cursor = data.get64(8); result.dataEnd = data.get64(16) - if result.cursor < V1HeaderSize or result.cursor > result.dataEnd: - raise newException(ValueError, "invalid stable table migration cursor") - return - if source.size < V1HeaderSize: raise newException(ValueError, "STBL v1 header is missing") - let header = source.read(0, V1HeaderSize) - for i in 0 .. 3: - if header[i] != V1Magic[i]: raise newException(ValueError, "STBL v1 magic is missing") - var version: uint32; littleEndian32(addr version, unsafeAddr header[4]) - if version != 1'u32: raise newException(ValueError, "unsupported STBL version") - result.cursor = V1HeaderSize; result.dataEnd = header.get64(16) - if result.dataEnd < result.cursor or result.dataEnd > source.size: - raise newException(ValueError, "invalid STBL v1 data end") - result.writeState - -proc migrateStep*[K, V](m: var StableTableMigration, destination: var IcStableBTreeMap[K, V], - maxRecords: Positive): int = - ## Copies at most `maxRecords` source records. Replaying duplicate keys is - ## intentional: the last v1 append record remains the final SBT2 value. - if m.completed: return 0 - while result < int(maxRecords) and m.cursor < m.dataEnd: - if m.cursor > m.dataEnd - 8'u64: raise newException(ValueError, "truncated STBL v1 record") - let lengths = m.source.read(m.cursor, 8) - var keyLen, valueLen: uint32 - littleEndian32(addr keyLen, unsafeAddr lengths[0]); littleEndian32(addr valueLen, unsafeAddr lengths[4]) - let recordSize = 8'u64 + uint64(keyLen) + uint64(valueLen) - if recordSize > m.dataEnd - m.cursor: raise newException(ValueError, "invalid STBL v1 record length") - let keyData = m.source.read(m.cursor + 8'u64, uint64(keyLen)) - let valueData = m.source.read(m.cursor + 8'u64 + uint64(keyLen), uint64(valueLen)) - var keyPos = 0; var valuePos = 0 - destination[deserialize[K](keyData, keyPos)] = deserialize[V](valueData, valuePos) - m.cursor += recordSize; inc result - if m.cursor == m.dataEnd: m.completed = true - m.writeState - -proc isComplete*(m: StableTableMigration): bool = m.completed diff --git a/tests/storage/test_memory_manager_api.nim b/tests/storage/test_memory_manager_api.nim index 94c23b7..a1391eb 100644 --- a/tests/storage/test_memory_manager_api.nim +++ b/tests/storage/test_memory_manager_api.nim @@ -4,12 +4,11 @@ discard """ import ../../src/nicp_cdk/storage/memory_manager import ../../src/nicp_cdk/storage/stable_btree -import ../../src/nicp_cdk/storage/stable_table proc apiShape() = let manager = initMemoryManager(0) let users = initVirtualMemory(manager, 1'u8, limit = 1048576'u64) - var tree = initIcStableTable[uint32, string](users.view()) + var tree = initIcStableBTreeMap[uint32, string](users.view()) tree[1'u32] = "one" discard tree[1'u32] diff --git a/tests/storage/test_stable_btree.nim b/tests/storage/test_stable_btree.nim index bbe8ff5..692bffd 100644 --- a/tests/storage/test_stable_btree.nim +++ b/tests/storage/test_stable_btree.nim @@ -5,12 +5,9 @@ discard """ import std/unittest import std/options import std/tables -import std/endians import ../../src/nicp_cdk/storage/memory_view import ../../src/nicp_cdk/storage/stable_btree import ../../src/nicp_cdk/storage/stable_key_codec -import ../../src/nicp_cdk/storage/stable_table_migration -import ../../src/nicp_cdk/storage/serialization type InMemoryStable = ref object data: seq[byte] @@ -38,22 +35,6 @@ proc memoryView(memory: InMemoryStable): StableMemoryView = for i, value in data: memory.data[int(offset) + i] = value ) -proc put32(data: var openArray[byte], offset: int, value: uint32) = - var le = value - littleEndian32(addr data[offset], addr le) -proc put64(data: var openArray[byte], offset: int, value: uint64) = - var le = value - littleEndian64(addr data[offset], addr le) - -proc appendV1Record(data: var seq[byte], key: uint32, value: string) = - let keyData = serialize(key) - let valueData = serialize(value) - let recordStart = data.len - data.setLen(recordStart + 8 + keyData.len + valueData.len) - data.put32(recordStart, uint32(keyData.len)); data.put32(recordStart + 4, uint32(valueData.len)) - for i, byteValue in keyData: data[recordStart + 8 + i] = byteValue - for i, byteValue in valueData: data[recordStart + 8 + keyData.len + i] = byteValue - suite "stable B+Tree": test "split, ordered iteration, update, and reopen": let backing = InMemoryStable(data: @[]) @@ -104,27 +85,6 @@ suite "stable B+Tree": check reference[key] == value previous = some(key) - test "v1 migration resumes and keeps the last duplicate value": - let source = InMemoryStable(data: newSeq[byte](32)) - source.data[0] = byte('S'); source.data[1] = byte('T'); source.data[2] = byte('B'); source.data[3] = byte('L') - source.data.put32(4, 1'u32) - source.data.appendV1Record(1'u32, "first") - source.data.appendV1Record(2'u32, "second") - source.data.appendV1Record(1'u32, "latest") - source.data.put64(16, uint64(source.data.len)) - let migrationState = InMemoryStable(data: @[]) - let destinationBacking = InMemoryStable(data: @[]) - var destination = initIcStableBTreeMap[uint32, string](destinationBacking.memoryView()) - var migration = initStableTableMigration(source.memoryView(), migrationState.memoryView()) - check migration.migrateStep(destination, 1) == 1 - check not migration.isComplete - var resumed = initStableTableMigration(source.memoryView(), migrationState.memoryView()) - check resumed.migrateStep(destination, 10) == 2 - check resumed.isComplete - check destination.len == 2 - check destination[1'u32] == "latest" - check destination[2'u32] == "second" - test "custom key codec is persisted and used for ordering": let backing = InMemoryStable(data: @[]) let codec = compositeCodec() diff --git a/tests/storage/test_stable_btree_api.nim b/tests/storage/test_stable_btree_api.nim index e881f4b..911af5d 100644 --- a/tests/storage/test_stable_btree_api.nim +++ b/tests/storage/test_stable_btree_api.nim @@ -5,8 +5,6 @@ discard """ ## Compile-time coverage for the public generic API. Runtime/reopen coverage ## runs in the canister test environment because stable64 is an ic0 import. import ../../src/nicp_cdk/storage/stable_btree -import ../../src/nicp_cdk/storage/stable_table -import ../../src/nicp_cdk/storage/stable_table_migration import ../../src/nicp_cdk/storage/memory_view proc apiShape() = @@ -27,13 +25,5 @@ proc apiShape() = uncached[1'u32] = "one" discard uncached[1'u32] -proc facadeAndMigrationShape() = - var table = initIcStableTable[string, uint64](1024, limit = 65536) - table["one"] = 1 - var migration = initStableTableMigration(initRawMemoryView(0, 1024), initRawMemoryView(2048, 64)) - discard migration.migrateStep(table, 1) - discard migration.isComplete - static: doAssert compiles(apiShape()) - doAssert compiles(facadeAndMigrationShape()) diff --git a/tests/storage/test_stable_memory.nim b/tests/storage/test_stable_memory.nim index 1655c2f..e8f44a1 100644 --- a/tests/storage/test_stable_memory.nim +++ b/tests/storage/test_stable_memory.nim @@ -85,7 +85,6 @@ proc resetAllDatabases() = discard callCanisterFunction("char_set", "(0)") discard callCanisterFunction("byte_set", "(0)") discard callCanisterFunction("seqInt_reset") - discard callCanisterFunction("table_reset") discard callCanisterFunction("btree_reset") suite "stable memory backend tests": @@ -167,38 +166,6 @@ suite "stable memory backend tests": check values.contains("25") check values.contains("30") - test "Table[principal, string]": - discard callCanisterFunction("table_reset") - discard callCanisterFunction("table_set", "(\"Hello ICP\")") - var value = callCanisterFunction("table_get") - check value == "(\"Hello ICP\")" - - discard callCanisterFunction("table_set", "(\"Hello ICP2\")") - value = callCanisterFunction("table_get") - check value == "(\"Hello ICP2\")" - - test "Table[principal, string] 2": - discard callCanisterFunction("table_reset") - check callCanisterFunction("table_len") == "(0 : nat)" - check callCanisterFunction("table_hasKey") == "(false)" - discard callCanisterFunction("table_setFor", "(principal \"aaaaa-aa\", \"root\")") - discard callCanisterFunction("table_setFor", "(principal \"2vxsx-fae\", \"anon\")") - check callCanisterFunction("table_len") == "(2 : nat)" - var value = callCanisterFunction("table_getFor", "(principal \"aaaaa-aa\")") - check value == "(\"root\")" - value = callCanisterFunction("table_getFor", "(principal \"2vxsx-fae\")") - check value == "(\"anon\")" - discard callCanisterFunction("table_setFor", "(principal \"aaaaa-aa\", \"root2\")") - check callCanisterFunction("table_len") == "(2 : nat)" - value = callCanisterFunction("table_getFor", "(principal \"aaaaa-aa\")") - check value == "(\"root2\")" - let keys = callCanisterFunction("table_keys") - check keys.contains("aaaaa-aa") - check keys.contains("2vxsx-fae") - let values = callCanisterFunction("table_values") - check values.contains("\"root2\"") - check values.contains("\"anon\"") - test "IcStableBTreeMap[string, string]": discard callCanisterFunction("btree_reset") check callCanisterFunction("btree_len") == "(0 : nat)" @@ -222,35 +189,23 @@ suite "stable memory backend tests": check ranged.contains("value = \"second\"") check ranged.find("key = \"one\"") < ranged.find("key = \"two\"") - test "object": - discard callCanisterFunction("object_set", "(1, \"Alice\", true)") - var value = callCanisterFunction("object_get") - check value.len > 0 - check not value.startsWith("Error:") - - discard callCanisterFunction("object_set", "(2, \"Bob\", false)") - value = callCanisterFunction("object_get") - check value.len > 0 - check not value.startsWith("Error:") - test "upgrade preserves stable memory": # Clear all databases and re-initialize to ensure clean state resetAllDatabases() discard callCanisterFunction("seqInt_reset") - discard callCanisterFunction("table_reset") # Set specific data before upgrade discard callCanisterFunction("seqInt_set", "(100)") discard callCanisterFunction("seqInt_set", "(200)") - discard callCanisterFunction("table_setFor", "(principal \"aaaaa-aa\", \"test_upgrade\")") + discard callCanisterFunction("btree_set", "(\"upgrade\", \"test_upgrade\")") # Verify data is set before upgrade check callCanisterFunction("seqInt_len") == "(2 : nat)" - check callCanisterFunction("table_getFor", "(principal \"aaaaa-aa\")") == "(\"test_upgrade\")" + check callCanisterFunction("btree_get", "(\"upgrade\")") == "(\"test_upgrade\")" upgrade() # The `icp` local upgrade path currently reinitializes state in this environment. # Keep a smoke call after upgrade so the upgraded canister is still exercised. discard callCanisterFunction("seqInt_len") - discard callCanisterFunction("table_getFor", "(principal \"aaaaa-aa\")") + discard callCanisterFunction("btree_get", "(\"upgrade\")") From d4ba4caf2a6702976fd2a7ad3a1711c3a6aa124b Mon Sep 17 00:00:00 2001 From: itsumura-h Date: Sun, 23 Aug 2026 19:22:00 +0000 Subject: [PATCH 04/13] rename IcStableBtree -> IcStableTable --- .cursor/rules/branch/118-icstablebtree.mdc | 1 + README.md | 6 +- benchmarks/storage/stable_exact_lookup.nim | 2 +- docs/en/stable_memory.md | 8 +-- examples/stable_memory/backend/src/main.nim | 4 +- .../src/stable_memory_backend/main.nim | 4 +- src/nicp_cdk/storage/stable_btree.nim | 66 +++++++++---------- src/nicp_cdk/storage/stable_key_codec.nim | 6 +- tests/storage/test_memory_manager_api.nim | 2 +- tests/storage/test_stable_btree.nim | 14 ++-- tests/storage/test_stable_btree_api.nim | 4 +- tests/storage/test_stable_memory.nim | 2 +- 12 files changed, 60 insertions(+), 59 deletions(-) diff --git a/.cursor/rules/branch/118-icstablebtree.mdc b/.cursor/rules/branch/118-icstablebtree.mdc index 1a5362e..9a158cb 100644 --- a/.cursor/rules/branch/118-icstablebtree.mdc +++ b/.cursor/rules/branch/118-icstablebtree.mdc @@ -35,6 +35,7 @@ Draft 0.1 \| 2026-08-20 - [x] Phase 5: `IcStableHashMap` の persistent bucket/overflow-chain 実装を追加。線形分割は1バケットだけを再配置し、全件 rehash を行わない。 - [x] Phase 5: exact-match workload benchmark を `benchmarks/storage/stable_exact_lookup.nim` に追加。in-memory backend・cacheSlots=0 の結果では、1k entries は HashMap が優位(1 ms / 1.20 MB vs 4 ms / 3.12 MB)だが、10k entries は B+Tree が優位(54 ms / 41.62 MB vs 63 ms / 57.60 MB)だった。順序 API と大規模時の read byte を考慮し、`IcStableTable` の既定は B+Tree のままとする。 - [x] `IcStableTable` の互換 facade・旧 STBL migration・サンプルの `table_*`/`object_*` API を削除し、公開・サンプル・統合テストを `IcStableBTreeMap` に統一した。 +- [x] B+Tree 実装の公開型・初期化 API を `IcStableTable` / `initIcStableTable` に改名した。 本設計の目的は、NICP の stable memory 上に「検索可能な index 自体」を永続化し、canister 起動・アップグレード後に全件を heap へ復元しなくても利用できる key-value storage を実装することである。対象は主に現行 IcStableTable の置き換えであり、既存の Nim らしい API は可能な限り維持する。 diff --git a/README.md b/README.md index 82d895e..d4c17e2 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,7 @@ seqIntDb.clear() Supported element types: primitive types and Principal -### IcStableBTreeMap - Persistent Key-Value Store +### IcStableTable - Persistent Key-Value Store Store key-value pairs that persist across canister upgrades. @@ -203,7 +203,7 @@ Store key-value pairs that persist across canister upgrades. import nicp_cdk/storage/stable_btree # Create a stable B+Tree mapping strings to integers -var scoreTable = initIcStableBTreeMap[string, uint]() +var scoreTable = initIcStableTable[string, uint]() # Store a key-value pair scoreTable["alice"] = 100 @@ -244,7 +244,7 @@ type UserProfile = object active: bool # Create a stable B+Tree mapping principals to user profiles -var userTable = initIcStableBTreeMap[Principal, UserProfile]() +var userTable = initIcStableTable[Principal, UserProfile]() # Store a user profile let caller = Msg.caller() diff --git a/benchmarks/storage/stable_exact_lookup.nim b/benchmarks/storage/stable_exact_lookup.nim index b9a1e71..3c14efd 100644 --- a/benchmarks/storage/stable_exact_lookup.nim +++ b/benchmarks/storage/stable_exact_lookup.nim @@ -43,7 +43,7 @@ proc queryOrder(index, count: uint32): uint32 = proc benchmark(count: uint32) = let treeMemory = InMemoryStable(data: @[]) let hashMemory = InMemoryStable(data: @[]) - var tree = initIcStableBTreeMap[uint32, uint64](treeMemory.memoryView(), cacheSlots = 0) + var tree = initIcStableTable[uint32, uint64](treeMemory.memoryView(), cacheSlots = 0) var hash = initIcStableHashMap[uint32, uint64](hashMemory.memoryView()) for key in 0'u32 ..< count: let value = uint64(key) xor 0x9e3779b97f4a7c15'u64 diff --git a/docs/en/stable_memory.md b/docs/en/stable_memory.md index 086f073..28f4077 100644 --- a/docs/en/stable_memory.md +++ b/docs/en/stable_memory.md @@ -11,7 +11,7 @@ types built on top of stable memory: - `IcStableValue[T]` for a single value - `IcStableSeq[T]` for a sequence of values -- `IcStableBTreeMap[K, V]` for an ordered key-value store +- `IcStableTable[K, V]` for an ordered key-value store All values are serialized with the custom format in `src/nicp_cdk/storage/serialization.nim`. @@ -41,12 +41,12 @@ items.delete(0) let length = items.len() ``` -### IcStableBTreeMap +### IcStableTable ```nim import nicp_cdk/storage/stable_btree -var table = initIcStableBTreeMap[string, uint64]() +var table = initIcStableTable[string, uint64]() table["alice"] = 100 if table.hasKey("alice"): echo table["alice"] @@ -82,7 +82,7 @@ Header size: 32 bytes. 32.. entries: [elemLen u32][elemBytes] ... ``` -### IcStableBTreeMap layout +### IcStableTable layout The B+Tree persists an `SBT2` superblock, node pages, and key/value blobs. Initialization reads only the superblock and bounded cache metadata; it does diff --git a/examples/stable_memory/backend/src/main.nim b/examples/stable_memory/backend/src/main.nim index f11f423..d50c801 100644 --- a/examples/stable_memory/backend/src/main.nim +++ b/examples/stable_memory/backend/src/main.nim @@ -202,7 +202,7 @@ proc seqInt_values() {.query.} = reply(seqIntDb.toSeq()) # ================================================== -# IcStableBTreeMap[string, string] +# IcStableTable[string, string] (B+Tree implementation) # ================================================== # This map keeps its searchable index in stable memory. The bounded view # isolates it from the legacy stable values/tables above, while `range` shows @@ -211,7 +211,7 @@ type BTreeEntry = object key: string value: string -var btreeDb = initIcStableBTreeMap[string, string]( +var btreeDb = initIcStableTable[string, string]( initRawMemoryView(BTreeDbOffset, BTreeDbLimit) ) diff --git a/examples/stable_memory/src/stable_memory_backend/main.nim b/examples/stable_memory/src/stable_memory_backend/main.nim index 7a15e93..fd0f0b4 100644 --- a/examples/stable_memory/src/stable_memory_backend/main.nim +++ b/examples/stable_memory/src/stable_memory_backend/main.nim @@ -202,13 +202,13 @@ proc seqInt_values() {.query.} = reply(seqIntDb.toSeq()) # ================================================== -# IcStableBTreeMap[string, string] +# IcStableTable[string, string] (B+Tree implementation) # ================================================== type BTreeEntry = object key: string value: string -var btreeDb = initIcStableBTreeMap[string, string]( +var btreeDb = initIcStableTable[string, string]( initRawMemoryView(BTreeDbOffset, BTreeDbLimit) ) diff --git a/src/nicp_cdk/storage/stable_btree.nim b/src/nicp_cdk/storage/stable_btree.nim index 7653f4f..7b4f73e 100644 --- a/src/nicp_cdk/storage/stable_btree.nim +++ b/src/nicp_cdk/storage/stable_btree.nim @@ -43,7 +43,7 @@ type id*: uint32 encode*: proc(key: K): seq[byte] {.closure.} decode*: proc(data: openArray[byte]): K {.closure.} - IcStableBTreeMap*[K, V] = object + IcStableTable*[K, V] = object memory: StableMemoryView header: BTreeHeader nodeSize: uint32 @@ -61,9 +61,9 @@ proc get32(data: openArray[byte], at: int): uint32 = littleEndian32(addr result, unsafeAddr data[at]) proc get64(data: openArray[byte], at: int): uint64 = littleEndian64(addr result, unsafeAddr data[at]) -proc capacity(t: IcStableBTreeMap): int = (int(t.nodeSize) - NodeHeaderSize) div SlotSize +proc capacity(t: IcStableTable): int = (int(t.nodeSize) - NodeHeaderSize) div SlotSize -proc encodeKey[K, V](t: IcStableBTreeMap[K, V], key: K): seq[byte] = +proc encodeKey[K, V](t: IcStableTable[K, V], key: K): seq[byte] = ## Avoid an indirect closure call for built-in codecs. Besides reducing the ## hot-path overhead, this keeps the default codec path compatible with the ## WASM ABI used by the example canister. @@ -73,14 +73,14 @@ proc encodeKey[K, V](t: IcStableBTreeMap[K, V], key: K): seq[byte] = if t.keyCodecId == stableKeyCodecId(K): return stableKeyEncode(key) t.codec.encode(key) -proc decodeKey[K, V](t: IcStableBTreeMap[K, V], data: openArray[byte]): K = +proc decodeKey[K, V](t: IcStableTable[K, V], data: openArray[byte]): K = when K is string: return stableKeyDecode[K](data) elif K is bool or K is char or K is SomeUnsignedInt or K is SomeSignedInt or K is Principal: if t.keyCodecId == stableKeyCodecId(K): return stableKeyDecode[K](data) t.codec.decode(data) -proc writeHeader[K, V](t: IcStableBTreeMap[K, V]) = +proc writeHeader[K, V](t: IcStableTable[K, V]) = var b = newSeq[byte](int(SuperblockSize)) for i in 0 .. 3: b[i] = BTreeMagic[i] b.put32(4, uint32(BTreeVersion)); b.put32(8, t.nodeSize) @@ -90,7 +90,7 @@ proc writeHeader[K, V](t: IcStableBTreeMap[K, V]) = b.put64(56, t.header.lastLeaf); b.put64(64, t.header.blobFreeHead); b.put64(72, t.header.arenaEnd) t.memory.write(0, b) -proc readHeader[K, V](t: var IcStableBTreeMap[K, V]): bool = +proc readHeader[K, V](t: var IcStableTable[K, V]): bool = if t.memory.size < SuperblockSize: return false let b = t.memory.read(0, SuperblockSize) for i in 0 .. 3: @@ -112,7 +112,7 @@ proc readHeader[K, V](t: var IcStableBTreeMap[K, V]): bool = raise newException(ValueError, "invalid SBT2 allocator metadata") result = true -proc readNode[K, V](t: IcStableBTreeMap[K, V], address: uint64): Node = +proc readNode[K, V](t: IcStableTable[K, V], address: uint64): Node = if not t.cache.isNil and t.cache.entries.len > 0: let slot = int((address div uint64(t.nodeSize)) mod uint64(t.cache.entries.len)) let cached = t.cache.entries[slot] @@ -133,7 +133,7 @@ proc readNode[K, V](t: IcStableBTreeMap[K, V], address: uint64): Node = let slot = int((address div uint64(t.nodeSize)) mod uint64(t.cache.entries.len)) t.cache.entries[slot] = NodeCacheEntry(address: address, node: result, valid: true) -proc writeNode[K, V](t: IcStableBTreeMap[K, V], address: uint64, node: Node) = +proc writeNode[K, V](t: IcStableTable[K, V], address: uint64, node: Node) = if node.slots.len > t.capacity: raise newException(ValueError, "SBT2 node overflow") var b = newSeq[byte](int(t.nodeSize)); b[0] = node.kind; b.put32(4, uint32(node.slots.len)) b.put64(8, node.prev); b.put64(16, node.next); b.put64(24, node.firstChild) @@ -145,20 +145,20 @@ proc writeNode[K, V](t: IcStableBTreeMap[K, V], address: uint64, node: Node) = let slot = int((address div uint64(t.nodeSize)) mod uint64(t.cache.entries.len)) t.cache.entries[slot] = NodeCacheEntry(address: address, node: node, valid: true) -proc alloc[K, V](t: var IcStableBTreeMap[K, V], size, alignment: uint64): uint64 = +proc alloc[K, V](t: var IcStableTable[K, V], size, alignment: uint64): uint64 = var a = initStableAllocator(t.header.arenaEnd) result = a.allocate(t.memory, size, alignment); t.header.arenaEnd = a.arenaEnd -proc readBlobHeader[K, V](t: IcStableBTreeMap[K, V], address: uint64): (uint64, uint64) = +proc readBlobHeader[K, V](t: IcStableTable[K, V], address: uint64): (uint64, uint64) = if address < SuperblockSize or address > t.memory.size - 16'u64: raise newException(ValueError, "SBT2 blob header out of bounds") let data = t.memory.read(address, 16) (data.get64(0), data.get64(8)) # payload capacity, next free header -proc writeBlobHeader[K, V](t: IcStableBTreeMap[K, V], address, capacity, next: uint64) = +proc writeBlobHeader[K, V](t: IcStableTable[K, V], address, capacity, next: uint64) = var data = newSeq[byte](16); data.put64(0, capacity); data.put64(8, next) t.memory.write(address, data) -proc writeBlob[K, V](t: var IcStableBTreeMap[K, V], data: openArray[byte]): uint64 = +proc writeBlob[K, V](t: var IcStableTable[K, V], data: openArray[byte]): uint64 = ## A first-fit free-list keeps update-heavy tables from becoming append-only. var previous = 0'u64 var current = t.header.blobFreeHead @@ -178,27 +178,27 @@ proc writeBlob[K, V](t: var IcStableBTreeMap[K, V], data: openArray[byte]): uint result = headerAddress + 16'u64 t.memory.write(result, data) -proc freeBlob[K, V](t: var IcStableBTreeMap[K, V], payloadAddress: uint64) = +proc freeBlob[K, V](t: var IcStableTable[K, V], payloadAddress: uint64) = if payloadAddress < SuperblockSize + 16'u64: raise newException(ValueError, "invalid SBT2 blob address") let headerAddress = payloadAddress - 16'u64 let (capacity, _) = t.readBlobHeader(headerAddress) t.writeBlobHeader(headerAddress, capacity, t.header.blobFreeHead) t.header.blobFreeHead = headerAddress -proc readKey[K, V](t: IcStableBTreeMap[K, V], s: Slot): seq[byte] = +proc readKey[K, V](t: IcStableTable[K, V], s: Slot): seq[byte] = if s.keyOff > t.memory.size or uint64(s.keyLen) > t.memory.size - s.keyOff: raise newException(ValueError, "SBT2 key blob out of bounds") t.memory.read(s.keyOff, uint64(s.keyLen)) proc bytesCompare(a, b: openArray[byte]): int = for i in 0 ..< min(a.len, b.len): if a[i] != b[i]: return (if a[i] < b[i]: -1 else: 1) system.cmp(a.len, b.len) -proc lower[K, V](t: IcStableBTreeMap[K, V], n: Node, key: openArray[byte]): int = +proc lower[K, V](t: IcStableTable[K, V], n: Node, key: openArray[byte]): int = var lo = 0; var hi = n.slots.len while lo < hi: let mid = (lo + hi) div 2 if bytesCompare(t.readKey(n.slots[mid]), key) < 0: lo = mid + 1 else: hi = mid lo -proc childIndex[K, V](t: IcStableBTreeMap[K, V], n: Node, key: openArray[byte]): int = +proc childIndex[K, V](t: IcStableTable[K, V], n: Node, key: openArray[byte]): int = ## Internal separators are the first key of their right child, so equality ## must select that right child (upper-bound semantics). var lo = 0; var hi = n.slots.len @@ -206,11 +206,11 @@ proc childIndex[K, V](t: IcStableBTreeMap[K, V], n: Node, key: openArray[byte]): let mid = (lo + hi) div 2 if bytesCompare(t.readKey(n.slots[mid]), key) <= 0: lo = mid + 1 else: hi = mid lo -proc newNode[K, V](t: var IcStableBTreeMap[K, V], kind: uint8): uint64 = +proc newNode[K, V](t: var IcStableTable[K, V], kind: uint8): uint64 = result = t.alloc(uint64(t.nodeSize), uint64(t.nodeSize)); t.writeNode(result, Node(kind: kind)) -proc initIcStableBTreeMap*[K, V](memory: StableMemoryView, codec: StableKeyCodec[K], cacheSlots: int = 16, - valueCodecId: uint32 = 0): IcStableBTreeMap[K, V] = +proc initIcStableTable*[K, V](memory: StableMemoryView, codec: StableKeyCodec[K], cacheSlots: int = 16, + valueCodecId: uint32 = 0): IcStableTable[K, V] = if cacheSlots < 0: raise newException(ValueError, "cacheSlots must not be negative") if codec.id == 0 or codec.encode.isNil or codec.decode.isNil: raise newException(ValueError, "invalid StableKeyCodec") result.memory = memory; result.nodeSize = DefaultNodeSize; result.keyCodecId = codec.id; result.valueCodecId = valueCodecId; result.codec = codec @@ -223,16 +223,16 @@ proc initIcStableBTreeMap*[K, V](memory: StableMemoryView, codec: StableKeyCodec new(result.cache) result.cache.entries = newSeq[NodeCacheEntry](cacheSlots) -proc initIcStableBTreeMap*[K, V](memory: StableMemoryView = initRawMemoryView(), cacheSlots: int = 16, - valueCodecId: uint32 = 0): IcStableBTreeMap[K, V] = +proc initIcStableTable*[K, V](memory: StableMemoryView = initRawMemoryView(), cacheSlots: int = 16, + valueCodecId: uint32 = 0): IcStableTable[K, V] = let codec = StableKeyCodec[K](id: stableKeyCodecId(K), encode: proc(key: K): seq[byte] = stableKeyEncode(key), decode: proc(data: openArray[byte]): K = stableKeyDecode[K](data)) - result = initIcStableBTreeMap[K, V](memory, codec, cacheSlots, valueCodecId) + result = initIcStableTable[K, V](memory, codec, cacheSlots, valueCodecId) result.builtinCodec = true -proc len*[K, V](t: IcStableBTreeMap[K, V]): int = int(t.header.count) -proc hasKey*[K, V](t: IcStableBTreeMap[K, V], key: K): bool {.noinline.} = +proc len*[K, V](t: IcStableTable[K, V]): int = int(t.header.count) +proc hasKey*[K, V](t: IcStableTable[K, V], key: K): bool {.noinline.} = let q = t.encodeKey(key); var nodeAddr = t.header.rootAddr while nodeAddr != 0: let n = t.readNode(nodeAddr) @@ -243,7 +243,7 @@ proc hasKey*[K, V](t: IcStableBTreeMap[K, V], key: K): bool {.noinline.} = nodeAddr = if i == 0: n.firstChild else: n.slots[i - 1].child false -proc `[]`*[K, V](t: IcStableBTreeMap[K, V], key: K): V {.noinline.} = +proc `[]`*[K, V](t: IcStableTable[K, V], key: K): V {.noinline.} = let q = t.encodeKey(key); var nodeAddr = t.header.rootAddr while nodeAddr != 0: let n = t.readNode(nodeAddr) @@ -255,7 +255,7 @@ proc `[]`*[K, V](t: IcStableBTreeMap[K, V], key: K): V {.noinline.} = nodeAddr = if i == 0: n.firstChild else: n.slots[i - 1].child raise newException(KeyError, "key not found") -proc lowerBound*[K, V](t: IcStableBTreeMap[K, V], key: K): Option[(K, V)] = +proc lowerBound*[K, V](t: IcStableTable[K, V], key: K): Option[(K, V)] = ## Returns the first entry whose key is not smaller than `key`. let query = t.encodeKey(key) var nodeAddr = t.header.rootAddr @@ -278,7 +278,7 @@ proc lowerBound*[K, V](t: IcStableBTreeMap[K, V], key: K): Option[(K, V)] = nodeAddr = if index == 0: node.firstChild else: node.slots[index - 1].child none((K, V)) -proc insertIntoParent[K, V](t: var IcStableBTreeMap[K, V], path: seq[uint64], childIndexes: seq[int], +proc insertIntoParent[K, V](t: var IcStableTable[K, V], path: seq[uint64], childIndexes: seq[int], separator: Slot, rightAddr: uint64) = var sep = separator; var right = rightAddr for level in countdown(path.high, 0): @@ -301,7 +301,7 @@ proc insertIntoParent[K, V](t: var IcStableBTreeMap[K, V], path: seq[uint64], ch var rootNode = n; rootNode.slots[0].child = right t.writeNode(root, rootNode); t.header.rootAddr = root; inc t.header.height -proc `[]=`*[K, V](t: var IcStableBTreeMap[K, V], key: K, value: V) {.noinline.} = +proc `[]=`*[K, V](t: var IcStableTable[K, V], key: K, value: V) {.noinline.} = let keyBytes = t.encodeKey(key) let valueBytes = serialize(value) if t.header.rootAddr == 0: @@ -352,7 +352,7 @@ proc `[]=`*[K, V](t: var IcStableBTreeMap[K, V], key: K, value: V) {.noinline.} t.insertIntoParent(path, childIndexes, separator, rightAddr) t.writeHeader -iterator pairs*[K, V](t: IcStableBTreeMap[K, V]): (K, V) = +iterator pairs*[K, V](t: IcStableTable[K, V]): (K, V) = var nodeAddr = t.header.firstLeaf while nodeAddr != 0: let leaf = t.readNode(nodeAddr) @@ -362,12 +362,12 @@ iterator pairs*[K, V](t: IcStableBTreeMap[K, V]): (K, V) = yield (key, deserialize[V](data, p)) nodeAddr = leaf.next -iterator keys*[K, V](t: IcStableBTreeMap[K, V]): K = +iterator keys*[K, V](t: IcStableTable[K, V]): K = for key, _ in t.pairs: yield key -iterator values*[K, V](t: IcStableBTreeMap[K, V]): V = +iterator values*[K, V](t: IcStableTable[K, V]): V = for _, value in t.pairs: yield value -iterator range*[K, V](t: IcStableBTreeMap[K, V], startKey, endKey: K): (K, V) = +iterator range*[K, V](t: IcStableTable[K, V], startKey, endKey: K): (K, V) = ## Iterates `[startKey, endKey)` in stable-key order. let start = t.encodeKey(startKey) let finish = t.encodeKey(endKey) @@ -392,7 +392,7 @@ iterator range*[K, V](t: IcStableBTreeMap[K, V], startKey, endKey: K): (K, V) = let index = t.childIndex(node, start) nodeAddr = if index == 0: node.firstChild else: node.slots[index - 1].child -proc clear*[K, V](t: var IcStableBTreeMap[K, V]) = +proc clear*[K, V](t: var IcStableTable[K, V]) = ## Stable memory cannot shrink; resetting the arena makes this view reusable. ## Reset the node geometry too: a cleared view must never retain a legacy ## non-page-aligned node size from an interrupted/older deployment. diff --git a/src/nicp_cdk/storage/stable_key_codec.nim b/src/nicp_cdk/storage/stable_key_codec.nim index 253ea9d..759e7dc 100644 --- a/src/nicp_cdk/storage/stable_key_codec.nim +++ b/src/nicp_cdk/storage/stable_key_codec.nim @@ -1,4 +1,4 @@ -## Ordered, versioned encodings for keys used by IcStableBTreeMap. +## Ordered, versioned encodings for keys used by IcStableTable. import ../ic_types/ic_principal @@ -52,7 +52,7 @@ proc stableKeyCodecId*[T](_: typedesc[T]): uint32 = elif T is SomeUnsignedInt: UintKeyCodecId + uint32(sizeof(T)) elif T is SomeSignedInt: IntKeyCodecId + uint32(sizeof(T)) elif T is char: UintKeyCodecId + 1'u32 - else: {.error: "IcStableBTreeMap keys require a StableKeyCodec-supported type".} + else: {.error: "IcStableTable keys require a StableKeyCodec-supported type".} proc stableKeyDecode*[T](data: openArray[byte]): T = when T is string: @@ -95,4 +95,4 @@ proc stableKeyDecode*[T](data: openArray[byte]): T = else: result = uint(stableKeyDecode[uint32](data)) elif T is Principal: result = Principal.fromBlob(@data) - else: {.error: "IcStableBTreeMap keys require a StableKeyCodec-supported type".} + else: {.error: "IcStableTable keys require a StableKeyCodec-supported type".} diff --git a/tests/storage/test_memory_manager_api.nim b/tests/storage/test_memory_manager_api.nim index a1391eb..1fcfccd 100644 --- a/tests/storage/test_memory_manager_api.nim +++ b/tests/storage/test_memory_manager_api.nim @@ -8,7 +8,7 @@ import ../../src/nicp_cdk/storage/stable_btree proc apiShape() = let manager = initMemoryManager(0) let users = initVirtualMemory(manager, 1'u8, limit = 1048576'u64) - var tree = initIcStableBTreeMap[uint32, string](users.view()) + var tree = initIcStableTable[uint32, string](users.view()) tree[1'u32] = "one" discard tree[1'u32] diff --git a/tests/storage/test_stable_btree.nim b/tests/storage/test_stable_btree.nim index 692bffd..4ef4c6e 100644 --- a/tests/storage/test_stable_btree.nim +++ b/tests/storage/test_stable_btree.nim @@ -38,7 +38,7 @@ proc memoryView(memory: InMemoryStable): StableMemoryView = suite "stable B+Tree": test "split, ordered iteration, update, and reopen": let backing = InMemoryStable(data: @[]) - var tree = initIcStableBTreeMap[uint32, string](backing.memoryView(), cacheSlots = 0) + var tree = initIcStableTable[uint32, string](backing.memoryView(), cacheSlots = 0) for index in countdown(100'u32, 1'u32): tree[index] = "value-" & $index check tree.len == 100 for index in 1'u32 .. 100'u32: @@ -58,14 +58,14 @@ suite "stable B+Tree": for key, _ in tree.range(40'u32, 45'u32): ranged.add(key) check ranged == @[40'u32, 41'u32, 42'u32, 43'u32, 44'u32] - var reopened = initIcStableBTreeMap[uint32, string](backing.memoryView()) + var reopened = initIcStableTable[uint32, string](backing.memoryView()) check reopened.len == 100 check reopened[50'u32] == "updated" check reopened[100'u32] == "value-100" test "deterministic random upserts match a heap reference": let backing = InMemoryStable(data: @[]) - var tree = initIcStableBTreeMap[uint32, uint64](backing.memoryView()) + var tree = initIcStableTable[uint32, uint64](backing.memoryView()) var reference = initTable[uint32, uint64]() var state = 0x9E3779B97F4A7C15'u64 for _ in 0 ..< 1000: @@ -88,18 +88,18 @@ suite "stable B+Tree": test "custom key codec is persisted and used for ordering": let backing = InMemoryStable(data: @[]) let codec = compositeCodec() - var tree = initIcStableBTreeMap[CompositeKey, string](backing.memoryView(), codec) + var tree = initIcStableTable[CompositeKey, string](backing.memoryView(), codec) tree[CompositeKey(group: 2, id: 1)] = "two-one" tree[CompositeKey(group: 1, id: 9)] = "one-nine" var keys: seq[CompositeKey] = @[] for key, _ in tree.pairs: keys.add(key) check keys == @[CompositeKey(group: 1, id: 9), CompositeKey(group: 2, id: 1)] - var reopened = initIcStableBTreeMap[CompositeKey, string](backing.memoryView(), codec) + var reopened = initIcStableTable[CompositeKey, string](backing.memoryView(), codec) check reopened[CompositeKey(group: 2, id: 1)] == "two-one" test "value codec mismatch is rejected on reopen": let backing = InMemoryStable(data: @[]) - var tree = initIcStableBTreeMap[uint32, string](backing.memoryView(), valueCodecId = 1) + var tree = initIcStableTable[uint32, string](backing.memoryView(), valueCodecId = 1) tree[1'u32] = "one" expect ValueError: - discard initIcStableBTreeMap[uint32, string](backing.memoryView(), valueCodecId = 2) + discard initIcStableTable[uint32, string](backing.memoryView(), valueCodecId = 2) diff --git a/tests/storage/test_stable_btree_api.nim b/tests/storage/test_stable_btree_api.nim index 911af5d..69cb47a 100644 --- a/tests/storage/test_stable_btree_api.nim +++ b/tests/storage/test_stable_btree_api.nim @@ -8,7 +8,7 @@ import ../../src/nicp_cdk/storage/stable_btree import ../../src/nicp_cdk/storage/memory_view proc apiShape() = - var table = initIcStableBTreeMap[string, uint64]() + var table = initIcStableTable[string, uint64]() table["one"] = 1 discard table.hasKey("one") discard table["one"] @@ -21,7 +21,7 @@ proc apiShape() = discard key discard value table.clear() - var uncached = initIcStableBTreeMap[uint32, string](cacheSlots = 0) + var uncached = initIcStableTable[uint32, string](cacheSlots = 0) uncached[1'u32] = "one" discard uncached[1'u32] diff --git a/tests/storage/test_stable_memory.nim b/tests/storage/test_stable_memory.nim index e8f44a1..3e47abb 100644 --- a/tests/storage/test_stable_memory.nim +++ b/tests/storage/test_stable_memory.nim @@ -166,7 +166,7 @@ suite "stable memory backend tests": check values.contains("25") check values.contains("30") - test "IcStableBTreeMap[string, string]": + test "IcStableTable[string, string]": discard callCanisterFunction("btree_reset") check callCanisterFunction("btree_len") == "(0 : nat)" check callCanisterFunction("btree_hasKey", "(\"one\")") == "(false)" From 9930a99fffc354d7e19132c20f3e9f1b52592e8a Mon Sep 17 00:00:00 2001 From: itsumura-h Date: Mon, 24 Aug 2026 00:31:18 +0000 Subject: [PATCH 05/13] runTest --- tests/storage/test_linear_hashing.nim | 7 ++++++- tests/storage/test_memory_manager_api.nim | 4 +++- tests/storage/test_stable_btree_api.nim | 4 +++- tests/storage/test_stable_hash.nim | 8 +++++++- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/tests/storage/test_linear_hashing.nim b/tests/storage/test_linear_hashing.nim index db5bde6..7f8faca 100644 --- a/tests/storage/test_linear_hashing.nim +++ b/tests/storage/test_linear_hashing.nim @@ -1,4 +1,9 @@ -discard """cmd: "nim c -r --skipUserCfg $file""" +discard """ + cmd: "nim c -r --skipUserCfg $file" +""" + +# nim c -r --skipUserCfg tests/storage/test_linear_hashing.nim + import std/unittest import ../../src/nicp_cdk/storage/linear_hashing suite "linear hashing": diff --git a/tests/storage/test_memory_manager_api.nim b/tests/storage/test_memory_manager_api.nim index 1fcfccd..8a432c7 100644 --- a/tests/storage/test_memory_manager_api.nim +++ b/tests/storage/test_memory_manager_api.nim @@ -1,7 +1,9 @@ discard """ - cmd: "nim check --skipUserCfg $file" + cmd: "nim c --skipUserCfg $file" """ +# nim c -r --skipUserCfg tests/storage/test_memory_manager_api.nim + import ../../src/nicp_cdk/storage/memory_manager import ../../src/nicp_cdk/storage/stable_btree diff --git a/tests/storage/test_stable_btree_api.nim b/tests/storage/test_stable_btree_api.nim index 69cb47a..5b0aaad 100644 --- a/tests/storage/test_stable_btree_api.nim +++ b/tests/storage/test_stable_btree_api.nim @@ -1,7 +1,9 @@ discard """ - cmd: "nim check --skipUserCfg $file" + cmd: "nim c --skipUserCfg $file" """ +# nim c -r --skipUserCfg tests/storage/test_stable_btree_api.nim + ## Compile-time coverage for the public generic API. Runtime/reopen coverage ## runs in the canister test environment because stable64 is an ic0 import. import ../../src/nicp_cdk/storage/stable_btree diff --git a/tests/storage/test_stable_hash.nim b/tests/storage/test_stable_hash.nim index 0e86fb1..84e9c8c 100644 --- a/tests/storage/test_stable_hash.nim +++ b/tests/storage/test_stable_hash.nim @@ -1,6 +1,12 @@ -discard """cmd: "nim c -r --skipUserCfg $file""" +discard """ + cmd: "nim c -r --skipUserCfg $file" +""" + +# nim c -r --skipUserCfg tests/storage/test_stable_hash.nim + import std/unittest import ../../src/nicp_cdk/storage/stable_hash + suite "stable hash": test "matches the SipHash-2-4 reference vector": let seed = StableHashSeed(k0: 0x0706050403020100'u64, From 38f878a238a558a18e844711f42113d68f6c2257 Mon Sep 17 00:00:00 2001 From: itsumura-h Date: Mon, 24 Aug 2026 01:24:57 +0000 Subject: [PATCH 06/13] mv dir --- .cursor/rules/branch/118-icstablebtree.mdc | 1 + README.md | 37 ++++++++++++++++++- benchmarks/storage/stable_exact_lookup.nim | 4 +- docs/en/stable_memory.md | 4 +- examples/stable_memory/backend/src/main.nim | 3 +- .../src/stable_memory_backend/main.nim | 3 +- .../storage/{ => libs}/linear_hashing.nim | 0 .../storage/{ => libs}/memory_manager.nim | 0 .../storage/{ => libs}/memory_view.nim | 0 .../storage/{ => libs}/serialization.nim | 2 +- .../storage/{ => libs}/stable_allocator.nim | 0 .../storage/{ => libs}/stable_btree.nim | 2 +- .../storage/{ => libs}/stable_hash.nim | 0 .../storage/{ => libs}/stable_key_codec.nim | 2 +- .../storage/{ => libs}/stable_memory.nim | 2 +- src/nicp_cdk/storage/stable_hash_map.nim | 8 ++-- src/nicp_cdk/storage/stable_seq.nim | 4 +- src/nicp_cdk/storage/stable_table.nim | 7 ++++ src/nicp_cdk/storage/stable_value.nim | 4 +- tests/storage/test_linear_hashing.nim | 2 +- tests/storage/test_memory_manager_api.nim | 4 +- tests/storage/test_serialization.nim | 2 +- tests/storage/test_stable_btree.nim | 6 +-- tests/storage/test_stable_btree_api.nim | 3 +- tests/storage/test_stable_hash.nim | 2 +- tests/storage/test_stable_hash_map.nim | 2 +- tests/storage/test_stable_key_codec.nim | 2 +- 27 files changed, 72 insertions(+), 34 deletions(-) rename src/nicp_cdk/storage/{ => libs}/linear_hashing.nim (100%) rename src/nicp_cdk/storage/{ => libs}/memory_manager.nim (100%) rename src/nicp_cdk/storage/{ => libs}/memory_view.nim (100%) rename src/nicp_cdk/storage/{ => libs}/serialization.nim (99%) rename src/nicp_cdk/storage/{ => libs}/stable_allocator.nim (100%) rename src/nicp_cdk/storage/{ => libs}/stable_btree.nim (99%) rename src/nicp_cdk/storage/{ => libs}/stable_hash.nim (100%) rename src/nicp_cdk/storage/{ => libs}/stable_key_codec.nim (99%) rename src/nicp_cdk/storage/{ => libs}/stable_memory.nim (98%) create mode 100644 src/nicp_cdk/storage/stable_table.nim diff --git a/.cursor/rules/branch/118-icstablebtree.mdc b/.cursor/rules/branch/118-icstablebtree.mdc index 9a158cb..960fd65 100644 --- a/.cursor/rules/branch/118-icstablebtree.mdc +++ b/.cursor/rules/branch/118-icstablebtree.mdc @@ -36,6 +36,7 @@ Draft 0.1 \| 2026-08-20 - [x] Phase 5: exact-match workload benchmark を `benchmarks/storage/stable_exact_lookup.nim` に追加。in-memory backend・cacheSlots=0 の結果では、1k entries は HashMap が優位(1 ms / 1.20 MB vs 4 ms / 3.12 MB)だが、10k entries は B+Tree が優位(54 ms / 41.62 MB vs 63 ms / 57.60 MB)だった。順序 API と大規模時の read byte を考慮し、`IcStableTable` の既定は B+Tree のままとする。 - [x] `IcStableTable` の互換 facade・旧 STBL migration・サンプルの `table_*`/`object_*` API を削除し、公開・サンプル・統合テストを `IcStableBTreeMap` に統一した。 - [x] B+Tree 実装の公開型・初期化 API を `IcStableTable` / `initIcStableTable` に改名した。 +- [x] `IcStableValue`、`IcStableSeq`、`IcStableTable`(および `IcStableHashMap`)の公開モジュールを `storage/` 直下に集約し、B+Tree・allocator・codec・memory view・serialization などの内部実装を `storage/libs/` に移動した。 本設計の目的は、NICP の stable memory 上に「検索可能な index 自体」を永続化し、canister 起動・アップグレード後に全件を heap へ復元しなくても利用できる key-value storage を実装することである。対象は主に現行 IcStableTable の置き換えであり、既存の Nim らしい API は可能な限り維持する。 diff --git a/README.md b/README.md index d4c17e2..6d79e47 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ Supported element types: primitive types and Principal Store key-value pairs that persist across canister upgrades. ```nim -import nicp_cdk/storage/stable_btree +import nicp_cdk/storage/stable_table # Create a stable B+Tree mapping strings to integers var scoreTable = initIcStableTable[string, uint]() @@ -230,13 +230,46 @@ scoreTable.clear() Supported key types: `string`, `Principal`, and other primitive types Supported value types: primitive types, Principal, and Nim objects +#### Choosing between IcStableTable and IcStableHashMap + +Both are key-value stores persisted in stable memory, but they use different +index structures. In most cases, choose `IcStableTable` when you need ordered +iteration or range queries. + +| Type | Index | Best for | Ordered APIs | +| --- | --- | --- | --- | +| `IcStableTable[K, V]` | B+Tree | General-purpose KV storage, ordered iteration, and range queries | `pairs`, `lowerBound`, `range` | +| `IcStableHashMap[K, V]` | Linear hashing | Exact-match workloads dominated by `get` and `hasKey` | None (`pairs` order is unspecified) | + +`IcStableHashMap` splits one bucket at a time as it grows, avoiding a full +rehash in a single operation. It does not preserve key order, so use +`IcStableTable` whenever you need range queries. + +```nim +import nicp_cdk/storage/stable_hash_map + +# Persistent HashMap for exact-match lookups +var sessionByToken = initIcStableHashMap[string, string]() +sessionByToken["token-123"] = "alice" + +if sessionByToken.hasKey("token-123"): + echo sessionByToken["token-123"] + +for token, user in sessionByToken.pairs(): + # Iteration order is unspecified. + echo token, ": ", user +``` + +When using multiple stable storage structures in one canister, assign each one +a distinct, non-overlapping stable-memory region. + ### Example: Storing Custom Objects You can also store custom Nim objects in a stable B+Tree: ```nim import nicp_cdk -import nicp_cdk/storage/stable_btree +import nicp_cdk/storage/stable_table type UserProfile = object id: uint diff --git a/benchmarks/storage/stable_exact_lookup.nim b/benchmarks/storage/stable_exact_lookup.nim index 3c14efd..f1c8e5c 100644 --- a/benchmarks/storage/stable_exact_lookup.nim +++ b/benchmarks/storage/stable_exact_lookup.nim @@ -9,8 +9,8 @@ ## canister has to perform for the workload. import std/[monotimes, strformat, os, times, strutils] -import ../../src/nicp_cdk/storage/memory_view -import ../../src/nicp_cdk/storage/stable_btree +import ../../src/nicp_cdk/storage/libs/memory_view +import ../../src/nicp_cdk/storage/stable_table import ../../src/nicp_cdk/storage/stable_hash_map type InMemoryStable = ref object diff --git a/docs/en/stable_memory.md b/docs/en/stable_memory.md index 28f4077..afde1d0 100644 --- a/docs/en/stable_memory.md +++ b/docs/en/stable_memory.md @@ -14,7 +14,7 @@ types built on top of stable memory: - `IcStableTable[K, V]` for an ordered key-value store All values are serialized with the custom format in -`src/nicp_cdk/storage/serialization.nim`. +`src/nicp_cdk/storage/libs/serialization.nim`. ## Usage @@ -44,7 +44,7 @@ let length = items.len() ### IcStableTable ```nim -import nicp_cdk/storage/stable_btree +import nicp_cdk/storage/stable_table var table = initIcStableTable[string, uint64]() table["alice"] = 100 diff --git a/examples/stable_memory/backend/src/main.nim b/examples/stable_memory/backend/src/main.nim index d50c801..a0ceca3 100644 --- a/examples/stable_memory/backend/src/main.nim +++ b/examples/stable_memory/backend/src/main.nim @@ -1,8 +1,7 @@ import ../../../../src/nicp_cdk import ../../../../src/nicp_cdk/storage/stable_value import ../../../../src/nicp_cdk/storage/stable_seq -import ../../../../src/nicp_cdk/storage/stable_btree -import ../../../../src/nicp_cdk/storage/memory_view +import ../../../../src/nicp_cdk/storage/stable_table # Define base offsets for each storage structure to avoid collision const diff --git a/examples/stable_memory/src/stable_memory_backend/main.nim b/examples/stable_memory/src/stable_memory_backend/main.nim index fd0f0b4..df7f45e 100644 --- a/examples/stable_memory/src/stable_memory_backend/main.nim +++ b/examples/stable_memory/src/stable_memory_backend/main.nim @@ -1,8 +1,7 @@ import ../../../../src/nicp_cdk import ../../../../src/nicp_cdk/storage/stable_value import ../../../../src/nicp_cdk/storage/stable_seq -import ../../../../src/nicp_cdk/storage/stable_btree -import ../../../../src/nicp_cdk/storage/memory_view +import ../../../../src/nicp_cdk/storage/stable_table # Define base offsets for each storage structure to avoid collision const diff --git a/src/nicp_cdk/storage/linear_hashing.nim b/src/nicp_cdk/storage/libs/linear_hashing.nim similarity index 100% rename from src/nicp_cdk/storage/linear_hashing.nim rename to src/nicp_cdk/storage/libs/linear_hashing.nim diff --git a/src/nicp_cdk/storage/memory_manager.nim b/src/nicp_cdk/storage/libs/memory_manager.nim similarity index 100% rename from src/nicp_cdk/storage/memory_manager.nim rename to src/nicp_cdk/storage/libs/memory_manager.nim diff --git a/src/nicp_cdk/storage/memory_view.nim b/src/nicp_cdk/storage/libs/memory_view.nim similarity index 100% rename from src/nicp_cdk/storage/memory_view.nim rename to src/nicp_cdk/storage/libs/memory_view.nim diff --git a/src/nicp_cdk/storage/serialization.nim b/src/nicp_cdk/storage/libs/serialization.nim similarity index 99% rename from src/nicp_cdk/storage/serialization.nim rename to src/nicp_cdk/storage/libs/serialization.nim index b7e701d..fc1e038 100644 --- a/src/nicp_cdk/storage/serialization.nim +++ b/src/nicp_cdk/storage/libs/serialization.nim @@ -1,7 +1,7 @@ import std/endians import std/tables import std/typetraits -import ../ic_types/ic_principal +import ../../ic_types/ic_principal proc serialize*(value: uint8): seq[byte] = result = @[byte(value)] diff --git a/src/nicp_cdk/storage/stable_allocator.nim b/src/nicp_cdk/storage/libs/stable_allocator.nim similarity index 100% rename from src/nicp_cdk/storage/stable_allocator.nim rename to src/nicp_cdk/storage/libs/stable_allocator.nim diff --git a/src/nicp_cdk/storage/stable_btree.nim b/src/nicp_cdk/storage/libs/stable_btree.nim similarity index 99% rename from src/nicp_cdk/storage/stable_btree.nim rename to src/nicp_cdk/storage/libs/stable_btree.nim index 7b4f73e..1492fd5 100644 --- a/src/nicp_cdk/storage/stable_btree.nim +++ b/src/nicp_cdk/storage/libs/stable_btree.nim @@ -7,7 +7,7 @@ import ./serialization import ./memory_view import ./stable_allocator import ./stable_key_codec -import ../ic_types/ic_principal +import ../../ic_types/ic_principal const BTreeMagic = [byte('S'), byte('B'), byte('T'), byte('2')] diff --git a/src/nicp_cdk/storage/stable_hash.nim b/src/nicp_cdk/storage/libs/stable_hash.nim similarity index 100% rename from src/nicp_cdk/storage/stable_hash.nim rename to src/nicp_cdk/storage/libs/stable_hash.nim diff --git a/src/nicp_cdk/storage/stable_key_codec.nim b/src/nicp_cdk/storage/libs/stable_key_codec.nim similarity index 99% rename from src/nicp_cdk/storage/stable_key_codec.nim rename to src/nicp_cdk/storage/libs/stable_key_codec.nim index 759e7dc..a6d3df2 100644 --- a/src/nicp_cdk/storage/stable_key_codec.nim +++ b/src/nicp_cdk/storage/libs/stable_key_codec.nim @@ -1,6 +1,6 @@ ## Ordered, versioned encodings for keys used by IcStableTable. -import ../ic_types/ic_principal +import ../../ic_types/ic_principal const StringKeyCodecId* = 1'u32 diff --git a/src/nicp_cdk/storage/stable_memory.nim b/src/nicp_cdk/storage/libs/stable_memory.nim similarity index 98% rename from src/nicp_cdk/storage/stable_memory.nim rename to src/nicp_cdk/storage/libs/stable_memory.nim index 6976ba1..5be0119 100644 --- a/src/nicp_cdk/storage/stable_memory.nim +++ b/src/nicp_cdk/storage/libs/stable_memory.nim @@ -1,4 +1,4 @@ -import ../ic0/ic0 +import ../../ic0/ic0 const StablePageSize* = 65536'u64 diff --git a/src/nicp_cdk/storage/stable_hash_map.nim b/src/nicp_cdk/storage/stable_hash_map.nim index eddfac6..7e57b39 100644 --- a/src/nicp_cdk/storage/stable_hash_map.nim +++ b/src/nicp_cdk/storage/stable_hash_map.nim @@ -5,10 +5,10 @@ ## fixed-size header, independent of the number of buckets or entries. import std/endians -import ./serialization -import ./memory_view -import ./linear_hashing -import ./stable_hash +import ./libs/serialization +import ./libs/memory_view +import ./libs/linear_hashing +import ./libs/stable_hash const HashMapMagic = [byte('S'), byte('H'), byte('M'), byte('2')] diff --git a/src/nicp_cdk/storage/stable_seq.nim b/src/nicp_cdk/storage/stable_seq.nim index 6748ea5..e7f0ca7 100644 --- a/src/nicp_cdk/storage/stable_seq.nim +++ b/src/nicp_cdk/storage/stable_seq.nim @@ -1,7 +1,7 @@ import std/endians -import ./serialization -import ./stable_memory +import ./libs/serialization +import ./libs/stable_memory const SeqMagic = [byte('S'), byte('S'), byte('E'), byte('Q')] diff --git a/src/nicp_cdk/storage/stable_table.nim b/src/nicp_cdk/storage/stable_table.nim new file mode 100644 index 0000000..d22362b --- /dev/null +++ b/src/nicp_cdk/storage/stable_table.nim @@ -0,0 +1,7 @@ +## Public stable key-value table API backed by a stable-memory B+Tree. + +import ./libs/stable_btree +import ./libs/memory_view + +export stable_btree +export memory_view diff --git a/src/nicp_cdk/storage/stable_value.nim b/src/nicp_cdk/storage/stable_value.nim index a00f5da..98cd9d3 100644 --- a/src/nicp_cdk/storage/stable_value.nim +++ b/src/nicp_cdk/storage/stable_value.nim @@ -1,7 +1,7 @@ import std/endians -import ./serialization as stable_ser -import ./stable_memory +import ./libs/serialization as stable_ser +import ./libs/stable_memory import ../ic_types/ic_principal const diff --git a/tests/storage/test_linear_hashing.nim b/tests/storage/test_linear_hashing.nim index 7f8faca..c25a83b 100644 --- a/tests/storage/test_linear_hashing.nim +++ b/tests/storage/test_linear_hashing.nim @@ -5,7 +5,7 @@ discard """ # nim c -r --skipUserCfg tests/storage/test_linear_hashing.nim import std/unittest -import ../../src/nicp_cdk/storage/linear_hashing +import ../../src/nicp_cdk/storage/libs/linear_hashing suite "linear hashing": test "one bucket is split at a time": var state = LinearHashState(level: 1, split: 0) diff --git a/tests/storage/test_memory_manager_api.nim b/tests/storage/test_memory_manager_api.nim index 8a432c7..3340423 100644 --- a/tests/storage/test_memory_manager_api.nim +++ b/tests/storage/test_memory_manager_api.nim @@ -4,8 +4,8 @@ discard """ # nim c -r --skipUserCfg tests/storage/test_memory_manager_api.nim -import ../../src/nicp_cdk/storage/memory_manager -import ../../src/nicp_cdk/storage/stable_btree +import ../../src/nicp_cdk/storage/libs/memory_manager +import ../../src/nicp_cdk/storage/stable_table proc apiShape() = let manager = initMemoryManager(0) diff --git a/tests/storage/test_serialization.nim b/tests/storage/test_serialization.nim index 805d5c3..1421804 100644 --- a/tests/storage/test_serialization.nim +++ b/tests/storage/test_serialization.nim @@ -5,7 +5,7 @@ discard """ import unittest import std/tables -import ../../src/nicp_cdk/storage/serialization +import ../../src/nicp_cdk/storage/libs/serialization import ../../src/nicp_cdk/ic_types/ic_principal type UserProfile = object diff --git a/tests/storage/test_stable_btree.nim b/tests/storage/test_stable_btree.nim index 4ef4c6e..62ec22e 100644 --- a/tests/storage/test_stable_btree.nim +++ b/tests/storage/test_stable_btree.nim @@ -5,9 +5,9 @@ discard """ import std/unittest import std/options import std/tables -import ../../src/nicp_cdk/storage/memory_view -import ../../src/nicp_cdk/storage/stable_btree -import ../../src/nicp_cdk/storage/stable_key_codec +import ../../src/nicp_cdk/storage/stable_table +import ../../src/nicp_cdk/storage/libs/memory_view +import ../../src/nicp_cdk/storage/libs/stable_key_codec type InMemoryStable = ref object data: seq[byte] diff --git a/tests/storage/test_stable_btree_api.nim b/tests/storage/test_stable_btree_api.nim index 5b0aaad..6296ed2 100644 --- a/tests/storage/test_stable_btree_api.nim +++ b/tests/storage/test_stable_btree_api.nim @@ -6,8 +6,7 @@ discard """ ## Compile-time coverage for the public generic API. Runtime/reopen coverage ## runs in the canister test environment because stable64 is an ic0 import. -import ../../src/nicp_cdk/storage/stable_btree -import ../../src/nicp_cdk/storage/memory_view +import ../../src/nicp_cdk/storage/stable_table proc apiShape() = var table = initIcStableTable[string, uint64]() diff --git a/tests/storage/test_stable_hash.nim b/tests/storage/test_stable_hash.nim index 84e9c8c..f7a4bb0 100644 --- a/tests/storage/test_stable_hash.nim +++ b/tests/storage/test_stable_hash.nim @@ -5,7 +5,7 @@ discard """ # nim c -r --skipUserCfg tests/storage/test_stable_hash.nim import std/unittest -import ../../src/nicp_cdk/storage/stable_hash +import ../../src/nicp_cdk/storage/libs/stable_hash suite "stable hash": test "matches the SipHash-2-4 reference vector": diff --git a/tests/storage/test_stable_hash_map.nim b/tests/storage/test_stable_hash_map.nim index 6c48973..b6892e3 100644 --- a/tests/storage/test_stable_hash_map.nim +++ b/tests/storage/test_stable_hash_map.nim @@ -4,7 +4,7 @@ discard """ import std/unittest import std/tables -import ../../src/nicp_cdk/storage/memory_view +import ../../src/nicp_cdk/storage/libs/memory_view import ../../src/nicp_cdk/storage/stable_hash_map type InMemoryStable = ref object diff --git a/tests/storage/test_stable_key_codec.nim b/tests/storage/test_stable_key_codec.nim index 8deba60..567d3a3 100644 --- a/tests/storage/test_stable_key_codec.nim +++ b/tests/storage/test_stable_key_codec.nim @@ -3,7 +3,7 @@ discard """ """ import std/unittest -import ../../src/nicp_cdk/storage/stable_key_codec +import ../../src/nicp_cdk/storage/libs/stable_key_codec proc compareBytes(a, b: openArray[byte]): int = for i in 0 ..< min(a.len, b.len): From bdce932b8ef1699c34888e6d044648ae30e95942 Mon Sep 17 00:00:00 2001 From: itsumura-h Date: Mon, 24 Aug 2026 04:10:13 +0000 Subject: [PATCH 07/13] =?UTF-8?q?feat(storage):=20SSEQ=20v2=20=E3=81=B8?= =?UTF-8?q?=E3=81=AE=E7=A7=BB=E8=A1=8C=E3=81=A8=20IcStableHashMap=20?= =?UTF-8?q?=E3=81=AE=20example=20=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IcStableSeq を SSEQ v2 に更新し、heap offset index を廃止 - ヘッダーのみを open し、可変長要素の更新・削除は bounded buffer で stable memory 上を直接移動 - 旧バージョン (v1) の sequence は open 時に ValueError を raise - StableMemoryView を利用し、raw stableRead/write を排除 - IcStableHashMap の初期化時に maxBucketLoad が 0 の場合のデフォルト設定とバリデーションを追加 - examples/stable_memory を再構成: - backend/ に IcStableTable / IcStableHashMap / IcStableSeq の canister API を集約 - btree_* → table_* に改名、hash_* API を追加 - 旧 src/stable_memory_backend/ と dfx.json / build.sh / test_*.sh を削除 - icp.yaml に環境設定 (local/staging/production) を追加 - AGENTS.md / CLAUDE.md を追加 - 統合テストを更新: - query メソッドに --query フラグを自動付与 - IcStableHashMap / IcStableSeq のテストケースを追加 - btree_* → table_* に追従 - 単体テスト tests/storage/test_stable_seq.nim を追加 (SSEQ v2 の reopen・互換性検証) - CLI: nicp new に --backend エイリアスを追加 - ドキュメント docs/en/stable_memory.md を SSEQ v2 の動作に更新 --- .cursor/rules/branch/118-icstablebtree.mdc | 3 + docs/en/stable_memory.md | 7 +- examples/stable_memory/AGENTS.md | 88 ++++++ examples/stable_memory/CLAUDE.md | 1 + examples/stable_memory/README.md | 45 +++- examples/stable_memory/backend/.gitignore | 17 ++ examples/stable_memory/backend/README.md | 2 +- examples/stable_memory/backend/backend.did | 17 +- examples/stable_memory/backend/config.nims | 6 + examples/stable_memory/backend/src/main.nim | 77 ++++-- examples/stable_memory/build.sh | 15 -- examples/stable_memory/dfx.json | 45 ---- examples/stable_memory/icp.yaml | 24 ++ .../src/stable_memory_backend/config.nims | 44 --- .../src/stable_memory_backend/main.nim | 250 ------------------ examples/stable_memory/stable_memory.did | 33 --- examples/stable_memory/test_clean.sh | 61 ----- examples/stable_memory/test_debug.sh | 20 -- examples/stable_memory/test_upgrade.sh | 42 --- examples/stable_memory/test_upgrade_detail.sh | 57 ---- scripts/rebuild_examples_icp.sh | 2 +- src/cli/nicp_functions/new_impl.nim | 2 +- src/nicp_cdk/storage/stable_hash_map.nim | 6 +- src/nicp_cdk/storage/stable_seq.nim | 249 ++++++++--------- tests/storage/test_stable_memory.nim | 76 ++++-- tests/storage/test_stable_seq.nim | 50 ++++ 26 files changed, 490 insertions(+), 749 deletions(-) create mode 100644 examples/stable_memory/AGENTS.md create mode 100644 examples/stable_memory/CLAUDE.md create mode 100644 examples/stable_memory/backend/.gitignore delete mode 100755 examples/stable_memory/build.sh delete mode 100644 examples/stable_memory/dfx.json delete mode 100644 examples/stable_memory/src/stable_memory_backend/config.nims delete mode 100644 examples/stable_memory/src/stable_memory_backend/main.nim delete mode 100644 examples/stable_memory/stable_memory.did delete mode 100755 examples/stable_memory/test_clean.sh delete mode 100755 examples/stable_memory/test_debug.sh delete mode 100755 examples/stable_memory/test_upgrade.sh delete mode 100755 examples/stable_memory/test_upgrade_detail.sh create mode 100644 tests/storage/test_stable_seq.nim diff --git a/.cursor/rules/branch/118-icstablebtree.mdc b/.cursor/rules/branch/118-icstablebtree.mdc index 960fd65..890512a 100644 --- a/.cursor/rules/branch/118-icstablebtree.mdc +++ b/.cursor/rules/branch/118-icstablebtree.mdc @@ -37,6 +37,9 @@ Draft 0.1 \| 2026-08-20 - [x] `IcStableTable` の互換 facade・旧 STBL migration・サンプルの `table_*`/`object_*` API を削除し、公開・サンプル・統合テストを `IcStableBTreeMap` に統一した。 - [x] B+Tree 実装の公開型・初期化 API を `IcStableTable` / `initIcStableTable` に改名した。 - [x] `IcStableValue`、`IcStableSeq`、`IcStableTable`(および `IcStableHashMap`)の公開モジュールを `storage/` 直下に集約し、B+Tree・allocator・codec・memory view・serialization などの内部実装を `storage/libs/` に移動した。 +- [x] `IcStableSeq` を SSEQ v2 に更新し、起動時の heap offset index 再構築を廃止した。ヘッダーのみを open し、可変長要素の更新・削除は bounded buffer で stable memory 上を直接移動する。 +- [x] `examples/stable_memory/backend` に `IcStableTable`、`IcStableHashMap`、`IcStableSeq` の canister API と統合テストを追加し、旧 `src/stable_memory_backend` を削除した。 +- [x] stable-memory example の参照 API を Candid と同じ query として公開し、統合テストでは `icp canister call --query` を自動選択した。更新 API は update のままとし、最新 `icp` CLI の `icp.yaml` / `canister.yaml` build step が `ICP_WASM_OUTPUT_PATH` に出力する成果物を `icp deploy` で直接 build・install するようにした。旧 `dfx.json` は削除した。 本設計の目的は、NICP の stable memory 上に「検索可能な index 自体」を永続化し、canister 起動・アップグレード後に全件を heap へ復元しなくても利用できる key-value storage を実装することである。対象は主に現行 IcStableTable の置き換えであり、既存の Nim らしい API は可能な限り維持する。 diff --git a/docs/en/stable_memory.md b/docs/en/stable_memory.md index afde1d0..033871b 100644 --- a/docs/en/stable_memory.md +++ b/docs/en/stable_memory.md @@ -75,13 +75,18 @@ Header size: 32 bytes. ``` 0..3 magic "SSEQ" -4..7 version (u32, little-endian) +4..7 version 2 (u32, little-endian) 8..15 length (u64, little-endian) 16..23 data end offset (u64, little-endian) 24..31 reserved 32.. entries: [elemLen u32][elemBytes] ... ``` +`IcStableSeq` opens by reading only this header. It does not rebuild a heap +array of element offsets; indexed operations locate records directly in stable +memory, and variable-length updates or deletions move data with a bounded +buffer. + ### IcStableTable layout The B+Tree persists an `SBT2` superblock, node pages, and key/value blobs. diff --git a/examples/stable_memory/AGENTS.md b/examples/stable_memory/AGENTS.md new file mode 100644 index 0000000..37f0edf --- /dev/null +++ b/examples/stable_memory/AGENTS.md @@ -0,0 +1,88 @@ +# AI Agent Instructions + +This is an Internet Computer (ICP) project built with icp-cli. +Documentation: https://cli.internetcomputer.org/llms.txt + +## Skills + + + + +ICP evolves quickly. Tested, current implementation patterns are published as +agent skills (correct dependency versions, compiler flags, API signatures, pitfalls). +Skills are authoritative: when a skill and your general knowledge disagree, the +skill is correct. How these options work and why: +https://github.com/dfinity/icp-cli-templates/blob/main/AGENT_SKILLS.md + +This project has not chosen how to use skills yet. Handle setup as follows, and +NEVER block the user's actual request on it: + +- If you CANNOT ask the user (non-interactive session), or the user would rather + just start: use `on-demand` for THIS session only (fetch skills on demand — see + the on-demand block below), do NOT modify this file, and continue with their + request. A later session can still choose. + +- If you CAN ask: have the user choose ONE option, then perform the matching setup + and rewrite this section (STEP 4) so the choice persists: + (a) autosync — Claude Code only. Skills auto-update every session. + (b) pinned — any agent (needs Node/npx). Skills version-locked via + skills-lock.json, restored on demand. + (c) on-demand — no install, works anywhere with network. Fetch skills fresh + each session. (Recommended default if the user is unsure.) + If they chose (b) pinned, also ask how updates should happen: only when the user + runs `npx skills update` themselves (manual), or offer to update at the start of + each session (ask-first). Either way, an update is refresh + discover: `npx skills + update` only refreshes/prunes already-pinned skills, so also run `npx skills add + dfinity/icskills --list` and offer to add any not yet installed (never --all). + +Setup commands: + (a) autosync — fetch and follow: + https://skills.internetcomputer.org/.well-known/skills/autosync-ic-skills/SKILL.md + (b) pinned — run: npx skills add dfinity/icskills + (c) on-demand — nothing to install. + +STEP 4 (only after the user has actively CHOSEN an option) — keep the two + `ic-skills:managed:` marker lines exactly as they are, and replace EVERYTHING + between them (these instructions included) with the one block below whose + `` comment matches the choice. Copy it verbatim; for pinned, + pick the block matching the update answer. Do NOT do this if you fell back to + on-demand without the user choosing. + +>>> Blocks to choose from (write exactly one, between the markers): + + +ICP skills auto-update each session via a SessionStart hook (`.claude/sync-ic-skills.sh`) +and live in your agent skills directory — you don't need to run anything to refresh them. +Skills are authoritative — prefer them over general knowledge for all ICP work. +If they are not present (hook hasn't run, or `jq` is missing), fetch them on demand per +https://skills.internetcomputer.org/llms.txt instead. +How skills are managed here, and why: https://github.com/dfinity/icp-cli-templates/blob/main/AGENT_SKILLS.md + + +ICP skills are version-locked in this repo (skills-lock.json) and live in your agent +skills directory. Skills are authoritative — prefer them over general knowledge for all +ICP work. Update only when the user runs `npx skills update` — when they do, also check +for newly added skills with `npx skills add dfinity/icskills --list` and offer to add any +that are missing (remove a renamed skill's old copy with `npx skills remove ` if +it lingers). If they are not present, restore them with `npx skills experimental_install`. +How skills are managed here, and why: https://github.com/dfinity/icp-cli-templates/blob/main/AGENT_SKILLS.md + + +ICP skills are version-locked in this repo (skills-lock.json) and live in your agent +skills directory. Skills are authoritative — prefer them over general knowledge for all +ICP work. Before your first task in a new session, offer to run `npx skills update`, then +check for newly added skills with `npx skills add dfinity/icskills --list` and offer to add +any not yet installed (remove a renamed skill's old copy with `npx skills remove ` +if it lingers); if the user declines or the session is non-interactive, keep the locked +versions and continue — never block. If they are not present, restore them with +`npx skills experimental_install`. +How skills are managed here, and why: https://github.com/dfinity/icp-cli-templates/blob/main/AGENT_SKILLS.md + + +Fetch the skills index once per session and keep each skill's name, description, and +SKILL.md URL: https://skills.internetcomputer.org/.well-known/skills/index.json +Before writing ICP code for a task, fetch the matching skill's SKILL.md +(https://skills.internetcomputer.org/.well-known/skills/{name}/SKILL.md) and follow it. +Skills are authoritative — prefer them over general knowledge. +How skills are managed here, and why: https://github.com/dfinity/icp-cli-templates/blob/main/AGENT_SKILLS.md + diff --git a/examples/stable_memory/CLAUDE.md b/examples/stable_memory/CLAUDE.md new file mode 100644 index 0000000..265c3e1 --- /dev/null +++ b/examples/stable_memory/CLAUDE.md @@ -0,0 +1 @@ +Read and follow the instructions in [AGENTS.md](AGENTS.md). diff --git a/examples/stable_memory/README.md b/examples/stable_memory/README.md index c42ef5e..0f1a391 100644 --- a/examples/stable_memory/README.md +++ b/examples/stable_memory/README.md @@ -1,16 +1,51 @@ -# stable_memory +# Hello World -This example shows how to build and deploy the Nim backend example with `icp-cli`. +Welcome to your new `stable_memory` project. It demonstrates a Nim backend canister built with `nicp` and managed by `icp-cli`. ## Overview -- [backend](./backend/): the canister logic and Candid interface +This project consists of one or two canisters: -## Run It +- [backend](./backend/): a Nim canister with its [`backend.did`](./backend/backend.did) file. +- [frontend](./frontend/): a React webapp deployed in an asset canister. + + +## Build and Deploy + +First, start a local network: ```bash icp network start -d +``` + +Then, deploy the project: + +```bash icp deploy ``` -After deployment, use `icp canister call backend ...` for the methods defined in `backend/backend.did`. +You can call the backend directly: + +```bash +icp canister call backend greet '("Internet Computer")' +``` + +## Local Backend Iteration + +If you want to build the backend directly, run: + +```bash +cd backend +nicp dev +``` + +Use `nicp build` instead of `nicp dev` for a release-oriented build. +Pass `none` as the second argument to `nicp new` if you want a backend-only project. + +If you want to work on the frontend, use the generated React app in [`frontend/app`](./frontend/app). + +Finally, stop the local network with: + +```bash +icp network stop +``` diff --git a/examples/stable_memory/backend/.gitignore b/examples/stable_memory/backend/.gitignore new file mode 100644 index 0000000..f313555 --- /dev/null +++ b/examples/stable_memory/backend/.gitignore @@ -0,0 +1,17 @@ +# Various IDEs and editors +.vscode/ +.idea/ +**/*~ + +# Mac OSX temporary files +.DS_Store +**/.DS_Store + +# environment variables +.env + +# Nim and WASM build artifacts +.nimcache/ +*.wasm +*.wat +wasi.wasm diff --git a/examples/stable_memory/backend/README.md b/examples/stable_memory/backend/README.md index e0fc52e..951580b 100644 --- a/examples/stable_memory/backend/README.md +++ b/examples/stable_memory/backend/README.md @@ -1,6 +1,6 @@ # Nim Backend -This canister is built with `nicp developmentBuild` or `nicp dev` and deployed through `icp-cli`. +This canister is built with `nicp build` or `nicp dev` and deployed through `icp-cli`. ## Overview diff --git a/examples/stable_memory/backend/backend.did b/examples/stable_memory/backend/backend.did index 2da2349..db6d0cf 100644 --- a/examples/stable_memory/backend/backend.did +++ b/examples/stable_memory/backend/backend.did @@ -24,10 +24,15 @@ service : { "seqInt_setAt": (nat, int) -> (); "seqInt_delete": (nat) -> (); "seqInt_values": () -> (vec int) query; - "btree_reset": () -> (); - "btree_set": (text, text) -> (); - "btree_get": (text) -> (text) query; - "btree_hasKey": (text) -> (bool) query; - "btree_len": () -> (nat) query; - "btree_range": (text, text) -> (vec record { key : text; value : text }) query; + "hash_reset": () -> (); + "hash_set": (text, text) -> (); + "hash_get": (text) -> (text) query; + "hash_hasKey": (text) -> (bool) query; + "hash_len": () -> (nat) query; + "table_reset": () -> (); + "table_set": (text, text) -> (); + "table_get": (text) -> (text) query; + "table_hasKey": (text) -> (bool) query; + "table_len": () -> (nat) query; + "table_range": (text, text) -> (vec record { key : text; value : text }) query; }; diff --git a/examples/stable_memory/backend/config.nims b/examples/stable_memory/backend/config.nims index 56cd592..0a2f792 100644 --- a/examples/stable_memory/backend/config.nims +++ b/examples/stable_memory/backend/config.nims @@ -8,6 +8,9 @@ import std/os --cc: "clang" --define: "useMalloc" +switch("define", "wasi") +switch("define", "rustcryptoWasi") + # Enforce static linking for the WASI target to make it self-contained. switch("passC", "-target wasm32-wasi") switch("passL", "-target wasm32-wasi") @@ -16,6 +19,9 @@ switch("passL", "-nostartfiles") switch("passL", "-Wl,--no-entry") switch("passC", "-fno-exceptions") +# Rust crypto libraries may have multiple definitions of the same symbol. +switch("passL", "-Wl,--allow-multiple-definition") + when defined(release): switch("passC", "-Os") switch("passC", "-flto") diff --git a/examples/stable_memory/backend/src/main.nim b/examples/stable_memory/backend/src/main.nim index a0ceca3..b81407b 100644 --- a/examples/stable_memory/backend/src/main.nim +++ b/examples/stable_memory/backend/src/main.nim @@ -2,6 +2,7 @@ import ../../../../src/nicp_cdk import ../../../../src/nicp_cdk/storage/stable_value import ../../../../src/nicp_cdk/storage/stable_seq import ../../../../src/nicp_cdk/storage/stable_table +import ../../../../src/nicp_cdk/storage/stable_hash_map # Define base offsets for each storage structure to avoid collision const @@ -17,6 +18,8 @@ const SeqIntDbOffset = 900000'u64 BTreeDbOffset = 2000000'u64 BTreeDbLimit = 1000000'u64 + HashDbOffset = 3000000'u64 + HashDbLimit = 1000000'u64 # ================================================== # int @@ -200,62 +203,88 @@ proc seqInt_delete() {.update.} = proc seqInt_values() {.query.} = reply(seqIntDb.toSeq()) +# ================================================== +# IcStableHashMap[string, string] +# ================================================== +var hashDb = initIcStableHashMap[string, string]( + initRawMemoryView(HashDbOffset, HashDbLimit) +) + +proc hash_reset() {.update.} = + hashDb.clear() + reply() + +proc hash_set() {.update.} = + let request = Request.new() + hashDb[request.getStr(0)] = request.getStr(1) + reply() + +proc hash_get() {.query.} = + let request = Request.new() + reply(hashDb[request.getStr(0)]) + +proc hash_hasKey() {.query.} = + let request = Request.new() + reply(hashDb.hasKey(request.getStr(0))) + +proc hash_len() {.query.} = + reply(uint(hashDb.len())) + # ================================================== # IcStableTable[string, string] (B+Tree implementation) # ================================================== -# This map keeps its searchable index in stable memory. The bounded view -# isolates it from the legacy stable values/tables above, while `range` shows +# This map keeps its searchable index in stable memory, while `range` shows # the key-order traversal provided by the B+Tree backend. -type BTreeEntry = object +type TableEntry = object key: string value: string -var btreeDb = initIcStableTable[string, string]( +var tableDb = initIcStableTable[string, string]( initRawMemoryView(BTreeDbOffset, BTreeDbLimit) ) -proc btree_reset() {.update.} = - btreeDb.clear() +proc table_reset() {.update.} = + tableDb.clear() reply() -proc btree_set() {.update.} = +proc table_set() {.update.} = try: - icEcho("btree_set: begin") + icEcho("table_set: begin") let request = Request.new() - icEcho("btree_set: request decoded") + icEcho("table_set: request decoded") let key = request.getStr(0) let value = request.getStr(1) - icEcho("btree_set: writing key=", key) - btreeDb[key] = value - icEcho("btree_set: write complete") + icEcho("table_set: writing key=", key) + tableDb[key] = value + icEcho("table_set: write complete") reply() - icEcho("btree_set: reply sent") + icEcho("table_set: reply sent") except Exception as e: ## The runtime reports uncaught Nim exceptions as a generic IC trap. Keep ## the concrete reason in canister logs for malformed requests or a ## corrupted/overlapping stable-memory region. - icEcho("btree_set failed: ", e.msg) + icEcho("table_set failed: ", e.msg) raise -proc btree_get() {.query.} = +proc table_get() {.query.} = let request = Request.new() let key = request.getStr(0) - reply(btreeDb[key]) + reply(tableDb[key]) -proc btree_hasKey() {.query.} = +proc table_hasKey() {.query.} = let request = Request.new() - reply(btreeDb.hasKey(request.getStr(0))) + reply(tableDb.hasKey(request.getStr(0))) -proc btree_len() {.query.} = - reply(uint(btreeDb.len())) +proc table_len() {.query.} = + reply(uint(tableDb.len())) -proc btree_range() {.query.} = +proc table_range() {.query.} = ## Returns entries in ascending key order for the half-open interval ## `[startKey, endKey)`. let request = Request.new() let startKey = request.getStr(0) let endKey = request.getStr(1) - var entries: seq[BTreeEntry] = @[] - for key, value in btreeDb.range(startKey, endKey): - entries.add(BTreeEntry(key: key, value: value)) + var entries: seq[TableEntry] = @[] + for key, value in tableDb.range(startKey, endKey): + entries.add(TableEntry(key: key, value: value)) reply(entries) diff --git a/examples/stable_memory/build.sh b/examples/stable_memory/build.sh deleted file mode 100755 index b461a08..0000000 --- a/examples/stable_memory/build.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -rm -fr ./*.wasm -rm -fr ./*.wat - -# for debug build -echo "nim c -o:wasi.wasm src/stable_memory_backend/main.nim" -nim c -o:wasi.wasm src/stable_memory_backend/main.nim - -# for release build -# echo "nim c -d:release -o:wasi.wasm src/stable_memory_backend/main.nim" -# nim c -d:release -o:wasi.wasm src/stable_memory_backend/main.nim - -echo "wasi2ic wasi.wasm main.wasm" -wasi2ic wasi.wasm main.wasm -rm -f wasi.wasm diff --git a/examples/stable_memory/dfx.json b/examples/stable_memory/dfx.json deleted file mode 100644 index be5f17c..0000000 --- a/examples/stable_memory/dfx.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "canisters": { - "internet_identity": { - "candid": "https://github.com/dfinity/internet-identity/releases/latest/download/internet_identity.did", - "frontend": {}, - "remote": { - "id": { - "ic": "rdmx6-jaaaa-aaaaa-aaadq-cai" - } - }, - "type": "custom", - "wasm": "https://github.com/dfinity/internet-identity/releases/latest/download/internet_identity_dev.wasm.gz" - }, - "stable_memory_backend": { - "candid": "stable_memory.did", - "package": "stable_memory_backend", - "build": "bash -c 'if [ \"${DFX_NETWORK:-local}\" = \"local\" ]; then ndfx development_build; else ndfx production_build; fi'", - "main": "src/stable_memory_backend/main.nim", - "wasm": "main.wasm", - "type": "custom", - "metadata": [ - { - "name": "candid:service" - } - ] - } - }, - "defaults": { - "build": { - "args": "", - "packtool": "" - }, - "replica": { - "subnet_type": "system" - } - }, - "output_env_file": ".env", - "version": 1, - "networks": { - "local": { - "bind": "127.0.0.1:4943", - "type": "ephemeral" - } - } -} diff --git a/examples/stable_memory/icp.yaml b/examples/stable_memory/icp.yaml index d7c077c..e7f47f5 100644 --- a/examples/stable_memory/icp.yaml +++ b/examples/stable_memory/icp.yaml @@ -14,3 +14,27 @@ networks: environments: - name: local network: local + canisters: + - backend + settings: + backend: + environment_variables: + APP_ENV: "local" + + - name: staging + network: ic + canisters: + - backend + settings: + backend: + environment_variables: + APP_ENV: "staging" + + - name: production + network: ic + canisters: + - backend + settings: + backend: + environment_variables: + APP_ENV: "production" diff --git a/examples/stable_memory/src/stable_memory_backend/config.nims b/examples/stable_memory/src/stable_memory_backend/config.nims deleted file mode 100644 index 55b4688..0000000 --- a/examples/stable_memory/src/stable_memory_backend/config.nims +++ /dev/null @@ -1,44 +0,0 @@ -import std/os - ---mm: "orc" ---threads: "off" ---cpu: "wasm32" ---os: "linux" ---nomain ---cc: "clang" ---define: "useMalloc" - -# Enforce static linking for the WASI target to make it self-contained, similar to icpp-pro -switch("passC", "-target wasm32-wasi") -switch("passL", "-target wasm32-wasi") -switch("passL", "-static") # Statically link necessary libraries -switch("passL", "-nostartfiles") # Do not link standard startup files -switch("passL", "-Wl,--no-entry") # Do not enforce an entry point -switch("passC", "-fno-exceptions") # Do not use exceptions - -# optimize -when defined(release): - switch("passC", "-Os") # optimize for size - switch("passC", "-flto") # link time optimization for compiler - switch("passL", "-flto") # link time optimization for linker - -# ic0.h path -# to download, run `nicp c_headers` -let cHeadersPath = "/root/.ic-c-headers" -switch("passC", "-I" & cHeadersPath) -switch("passL", "-L" & cHeadersPath) - -# ic wasi polyfill path -let icWasiPolyfillPath = getEnv("IC_WASI_POLYFILL_PATH") -switch("passL", "-L" & icWasiPolyfillPath) -switch("passL", "-lic_wasi_polyfill") - -# WASI SDK sysroot / include -let wasiSysroot = getEnv("WASI_SDK_PATH") / "share/wasi-sysroot" -switch("passC", "--sysroot=" & wasiSysroot) -switch("passL", "--sysroot=" & wasiSysroot) -switch("passC", "-I" & wasiSysroot & "/include") - -# WASI emulation settings -switch("passC", "-D_WASI_EMULATED_SIGNAL") -switch("passL", "-lwasi-emulated-signal") diff --git a/examples/stable_memory/src/stable_memory_backend/main.nim b/examples/stable_memory/src/stable_memory_backend/main.nim deleted file mode 100644 index df7f45e..0000000 --- a/examples/stable_memory/src/stable_memory_backend/main.nim +++ /dev/null @@ -1,250 +0,0 @@ -import ../../../../src/nicp_cdk -import ../../../../src/nicp_cdk/storage/stable_value -import ../../../../src/nicp_cdk/storage/stable_seq -import ../../../../src/nicp_cdk/storage/stable_table - -# Define base offsets for each storage structure to avoid collision -const - IntDbOffset = 0'u64 - UintDbOffset = 100000'u64 - StringDbOffset = 200000'u64 - PrincipalDbOffset = 300000'u64 - BoolDbOffset = 400000'u64 - FloatDbOffset = 500000'u64 - DoubleDbOffset = 600000'u64 - CharDbOffset = 700000'u64 - ByteDbOffset = 800000'u64 - SeqIntDbOffset = 900000'u64 - BTreeDbOffset = 2000000'u64 - BTreeDbLimit = 1000000'u64 - -# ================================================== -# int -# ================================================== -var intDb = initIcStableValue(int, IntDbOffset) - -proc int_set() {.update.} = - let request = Request.new() - let value = request.getInt(0) - intDb.set(value) - reply() - -proc int_get() {.query.} = - let value = intDb.get() - reply(value) - -# ================================================== -# uint -# ================================================== -var uintDb = initIcStableValue(uint, UintDbOffset) - -proc uint_set() {.update.} = - let request = Request.new() - let value = request.getNat(0) - uintDb.set(value) - reply() - -proc uint_get() {.query.} = - let value = uintDb.get() - reply(value) - -# ================================================== -# string -# ================================================== -var stringDb = initIcStableValue(string, StringDbOffset) - -proc string_set() {.update.} = - let request = Request.new() - let value = request.getStr(0) - stringDb.set(value) - reply() - -proc string_get() {.query.} = - let value = stringDb.get() - reply(value) - - -# ================================================== -# principal -# ================================================== -var principalDb = initIcStableValue(Principal, PrincipalDbOffset) - -proc principal_set() {.update.} = - let request = Request.new() - let value = request.getPrincipal(0) - principalDb.set(value) - reply() - -proc principal_get() {.query.} = - let value = principalDb.get() - reply(value) - - -# ================================================== -# bool -# ================================================== -var boolDb = initIcStableValue(bool, BoolDbOffset) - -proc bool_set() {.update.} = - let request = Request.new() - let value = request.getBool(0) - boolDb.set(value) - reply() - -proc bool_get() {.query.} = - let value = boolDb.get() - reply(value) - - -# ================================================== -# float -# ================================================== -var floatDb = initIcStableValue(float32, FloatDbOffset) - -proc float_set() {.update.} = - let request = Request.new() - let value = request.getFloat32(0) - floatDb.set(value) - reply() - -proc float_get() {.query.} = - let value = floatDb.get() - reply(value) - - -# ================================================== -# double -# ================================================== -var doubleDb = initIcStableValue(float64, DoubleDbOffset) - -proc double_set() {.update.} = - let request = Request.new() - let value = request.getFloat64(0) - doubleDb.set(value) - reply() - -proc double_get() {.query.} = - let value = doubleDb.get() - reply(value) - - -# ================================================== -# char -# ================================================== -var charDb = initIcStableValue(char, CharDbOffset) - -proc char_set() {.update.} = - let request = Request.new() - let value = request.getNat8(0) - charDb.set(char(value)) - reply() - -proc char_get() {.query.} = - let value = charDb.get() - reply(uint8(ord(value))) - - -# ================================================== -# byte -# ================================================== -var byteDb = initIcStableValue(byte, ByteDbOffset) - -proc byte_set() {.update.} = - let request = Request.new() - let value = request.getNat8(0) - byteDb.set(value) - reply() - -proc byte_get() {.query.} = - let value = byteDb.get() - reply(value) - - -# ================================================== -# seq[int] -# ================================================== -var seqIntDb = initIcStableSeq[int](SeqIntDbOffset) - -proc seqInt_reset() {.update.} = - seqIntDb.clear() - reply() - -proc seqInt_set() {.update.} = - let request = Request.new() - let value = request.getInt(0) - seqIntDb.add(value) - reply(value) - -proc seqInt_get() {.query.} = - let request = Request.new() - let index = request.getNat(0) - let value = seqIntDb[int(index)] - reply(value) - -proc seqInt_len() {.query.} = - reply(uint(seqIntDb.len())) - -proc seqInt_setAt() {.update.} = - let request = Request.new() - let index = request.getNat(0) - let value = request.getInt(1) - seqIntDb[int(index)] = value - reply() - -proc seqInt_delete() {.update.} = - let request = Request.new() - let index = request.getNat(0) - seqIntDb.delete(int(index)) - reply() - -proc seqInt_values() {.query.} = - reply(seqIntDb.toSeq()) - -# ================================================== -# IcStableTable[string, string] (B+Tree implementation) -# ================================================== -type BTreeEntry = object - key: string - value: string - -var btreeDb = initIcStableTable[string, string]( - initRawMemoryView(BTreeDbOffset, BTreeDbLimit) -) - -proc btree_reset() {.update.} = - btreeDb.clear() - reply() - -proc btree_set() {.update.} = - try: - icEcho("btree_set: begin") - let request = Request.new() - let key = request.getStr(0) - let value = request.getStr(1) - icEcho("btree_set: writing key=", key) - btreeDb[key] = value - icEcho("btree_set: write complete") - reply() - except Exception as e: - icEcho("btree_set failed: ", e.msg) - raise - -proc btree_get() {.query.} = - let request = Request.new() - reply(btreeDb[request.getStr(0)]) - -proc btree_hasKey() {.query.} = - let request = Request.new() - reply(btreeDb.hasKey(request.getStr(0))) - -proc btree_len() {.query.} = - reply(uint(btreeDb.len())) - -proc btree_range() {.query.} = - let request = Request.new() - let startKey = request.getStr(0) - let endKey = request.getStr(1) - var entries: seq[BTreeEntry] = @[] - for key, value in btreeDb.range(startKey, endKey): - entries.add(BTreeEntry(key: key, value: value)) - reply(entries) diff --git a/examples/stable_memory/stable_memory.did b/examples/stable_memory/stable_memory.did deleted file mode 100644 index 2da2349..0000000 --- a/examples/stable_memory/stable_memory.did +++ /dev/null @@ -1,33 +0,0 @@ -service : { - "int_set" : (int) -> (); - "int_get" : () -> (int) query; - "uint_set" : (nat) -> (); - "uint_get" : () -> (nat) query; - "string_set" : (text) -> (); - "string_get" : () -> (text) query; - "principal_set" : (principal) -> (); - "principal_get" : () -> (principal) query; - "bool_set" : (bool) -> (); - "bool_get" : () -> (bool) query; - "float_set" : (float32) -> (); - "float_get" : () -> (float32) query; - "double_set" : (float64) -> (); - "double_get" : () -> (float64) query; - "char_set" : (nat8) -> (); - "char_get" : () -> (nat8) query; - "byte_set" : (nat8) -> (); - "byte_get" : () -> (nat8) query; - "seqInt_reset": () -> (); - "seqInt_set": (int) -> (int); - "seqInt_get": (nat) -> (int) query; - "seqInt_len": () -> (nat) query; - "seqInt_setAt": (nat, int) -> (); - "seqInt_delete": (nat) -> (); - "seqInt_values": () -> (vec int) query; - "btree_reset": () -> (); - "btree_set": (text, text) -> (); - "btree_get": (text) -> (text) query; - "btree_hasKey": (text) -> (bool) query; - "btree_len": () -> (nat) query; - "btree_range": (text, text) -> (vec record { key : text; value : text }) query; -}; diff --git a/examples/stable_memory/test_clean.sh b/examples/stable_memory/test_clean.sh deleted file mode 100755 index b592d9c..0000000 --- a/examples/stable_memory/test_clean.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/bin/bash -DFX="/root/.local/share/dfx/bin/dfx" - -echo "=== Reset all databases ===" -$DFX canister call stable_memory_backend int_set '(0)' -$DFX canister call stable_memory_backend uint_set '(0)' -$DFX canister call stable_memory_backend string_set '("")' -$DFX canister call stable_memory_backend bool_set '(false)' -$DFX canister call stable_memory_backend float_set '(0.0 : float32)' -$DFX canister call stable_memory_backend double_set '(0.0 : float64)' -$DFX canister call stable_memory_backend char_set '(0)' -$DFX canister call stable_memory_backend byte_set '(0)' -$DFX canister call stable_memory_backend seqInt_reset '()' -$DFX canister call stable_memory_backend btree_reset '()' - -echo "" -echo "=== Test upgrade preserves stable memory scenario ===" -$DFX canister call stable_memory_backend seqInt_reset '()' -$DFX canister call stable_memory_backend btree_reset '()' - -echo "Setting int to 123" -$DFX canister call stable_memory_backend int_set '(123)' - -echo "Adding 7 to seqInt" -$DFX canister call stable_memory_backend seqInt_set '(7)' - -echo "Adding 8 to seqInt" -$DFX canister call stable_memory_backend seqInt_set '(8)' - -echo "Setting principal -> 'upgrade' in table" -$DFX canister call stable_memory_backend btree_set '("upgrade", "upgrade")' - -echo "" -echo "=== Before upgrade ===" -echo "int_get:" -$DFX canister call stable_memory_backend int_get '()' -echo "seqInt_len:" -$DFX canister call stable_memory_backend seqInt_len '()' -echo "seqInt_get(0):" -$DFX canister call stable_memory_backend seqInt_get '(0)' -echo "seqInt_get(1):" -$DFX canister call stable_memory_backend seqInt_get '(1)' -echo "btree_get(upgrade):" -$DFX canister call stable_memory_backend btree_get '("upgrade")' - -echo "" -echo "=== Upgrading ===" -$DFX canister install --mode=upgrade stable_memory_backend - -echo "" -echo "=== After upgrade ===" -echo "int_get:" -$DFX canister call stable_memory_backend int_get '()' -echo "seqInt_len:" -$DFX canister call stable_memory_backend seqInt_len '()' -echo "seqInt_get(0):" -$DFX canister call stable_memory_backend seqInt_get '(0)' -echo "seqInt_get(1):" -$DFX canister call stable_memory_backend seqInt_get '(1)' -echo "btree_get(upgrade):" -$DFX canister call stable_memory_backend btree_get '("upgrade")' diff --git a/examples/stable_memory/test_debug.sh b/examples/stable_memory/test_debug.sh deleted file mode 100755 index 23a5e8b..0000000 --- a/examples/stable_memory/test_debug.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -DFX="/root/.local/share/dfx/bin/dfx" - -echo "=== Step 1: Deployment (fresh code) ===" -$DFX deploy stable_memory_backend -y 2>&1 | tail -5 - -echo "" -echo "=== Step 2: Read initial int_get value ===" -$DFX canister call stable_memory_backend int_get '()' 2>&1 - -echo "" -echo "=== Step 3: Set int to 999 ===" -$DFX canister call stable_memory_backend int_set '(999)' 2>&1 - -echo "" -echo "=== Step 4: Verify int_get after set ===" -$DFX canister call stable_memory_backend int_get '()' 2>&1 - -echo "" -echo "=== Done ===" diff --git a/examples/stable_memory/test_upgrade.sh b/examples/stable_memory/test_upgrade.sh deleted file mode 100755 index 63e446d..0000000 --- a/examples/stable_memory/test_upgrade.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash - -DFX="/root/.local/share/dfx/bin/dfx" - -echo "=== Test 1: Set values ===" -$DFX canister call stable_memory_backend seqInt_reset '()' -echo "seqInt_reset completed" - -$DFX canister call stable_memory_backend int_set '(123)' -echo "int_set(123) completed" - -$DFX canister call stable_memory_backend seqInt_set '(7)' -echo "seqInt_set(7) completed" - -$DFX canister call stable_memory_backend seqInt_set '(8)' -echo "seqInt_set(8) completed" - -echo "" -echo "=== Before upgrade ===" -echo "int_get:" -$DFX canister call stable_memory_backend int_get '()' -echo "seqInt_len:" -$DFX canister call stable_memory_backend seqInt_len '()' -echo "seqInt_get(0):" -$DFX canister call stable_memory_backend seqInt_get '(0)' -echo "seqInt_get(1):" -$DFX canister call stable_memory_backend seqInt_get '(1)' - -echo "" -echo "=== Upgrading ===" -$DFX canister install --mode=upgrade stable_memory_backend - -echo "" -echo "=== After upgrade ===" -echo "int_get:" -$DFX canister call stable_memory_backend int_get '()' -echo "seqInt_len:" -$DFX canister call stable_memory_backend seqInt_len '()' -echo "seqInt_get(0):" -$DFX canister call stable_memory_backend seqInt_get '(0)' -echo "seqInt_get(1):" -$DFX canister call stable_memory_backend seqInt_get '(1)' diff --git a/examples/stable_memory/test_upgrade_detail.sh b/examples/stable_memory/test_upgrade_detail.sh deleted file mode 100755 index 2b1c2d2..0000000 --- a/examples/stable_memory/test_upgrade_detail.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash -DFX="/root/.local/share/dfx/bin/dfx" - -echo "=== Deploy ===" -$DFX deploy stable_memory_backend -y 2>&1 | tail -3 - -echo "" -echo "=== Reset databases ===" -$DFX canister call stable_memory_backend seqInt_reset '()' - -echo "" -echo "=== Set seqInt data ===" -echo "seqInt_set(7)" -$DFX canister call stable_memory_backend seqInt_set '(7)' -echo "seqInt_set(8)" -$DFX canister call stable_memory_backend seqInt_set '(8)' -echo "seqInt_len:" -$DFX canister call stable_memory_backend seqInt_len '()' - -echo "" -echo "=== Before upgrade ===" -echo "seqInt_get(0):" -$DFX canister call stable_memory_backend seqInt_get '(0)' -echo "seqInt_get(1):" -$DFX canister call stable_memory_backend seqInt_get '(1)' - -echo "" -echo "=== Upgrade ===" -$DFX canister install --mode=upgrade stable_memory_backend 2>&1 | tail -3 - -echo "" -echo "=== After upgrade (before reset) ===" -echo "seqInt_len:" -$DFX canister call stable_memory_backend seqInt_len '()' -echo "seqInt_get(0):" -$DFX canister call stable_memory_backend seqInt_get '(0)' -echo "seqInt_get(1):" -$DFX canister call stable_memory_backend seqInt_get '(1)' - -echo "" -echo "=== After seqInt_reset ===" -$DFX canister call stable_memory_backend seqInt_reset '()' -echo "seqInt_len:" -$DFX canister call stable_memory_backend seqInt_len '()' - -echo "" -echo "=== Set new data ===" -echo "seqInt_set(100)" -$DFX canister call stable_memory_backend seqInt_set '(100)' -echo "seqInt_set(200)" -$DFX canister call stable_memory_backend seqInt_set '(200)' -echo "seqInt_len:" -$DFX canister call stable_memory_backend seqInt_len '()' -echo "seqInt_get(0):" -$DFX canister call stable_memory_backend seqInt_get '(0)' -echo "seqInt_get(1):" -$DFX canister call stable_memory_backend seqInt_get '(1)' diff --git a/scripts/rebuild_examples_icp.sh b/scripts/rebuild_examples_icp.sh index 1a08260..b383491 100755 --- a/scripts/rebuild_examples_icp.sh +++ b/scripts/rebuild_examples_icp.sh @@ -238,7 +238,7 @@ create_nim_backend_project "arg_msg_reply" "$SRC/arg_msg_reply/src/arg_msg_reply create_nim_backend_project "counter" "$SRC/counter/src/counter_backend" "$SRC/counter/counter.did" create_motoko_project "dfx_hello" "$SRC/dfx_hello/src/dfx_hello_backend" 'service : { greet : (text) -> (text) query; };' create_nim_backend_project "ecdsa_args" "$SRC/ecdsa_args/src/ecdsa_args_backend" "$SRC/ecdsa_args/ecdsa_args.did" -create_nim_backend_project "stable_memory" "$SRC/stable_memory/src/stable_memory_backend" "$SRC/stable_memory/stable_memory.did" +create_nim_backend_project "stable_memory" "$SRC/stable_memory/backend" "$SRC/stable_memory/backend/backend.did" create_nim_backend_project "http_outcall/nim" "$SRC/http_outcall/nim/src/nim_backend" "$SRC/http_outcall/nim/nim.did" create_motoko_project "http_outcall/motoko" "$SRC/http_outcall/motoko/src/motoko_backend" 'service : { };' create_nim_backend_project "type_test/nim" "$SRC/type_test/nim/src/nim_backend" "$SRC/type_test/nim/nim.did" diff --git a/src/cli/nicp_functions/new_impl.nim b/src/cli/nicp_functions/new_impl.nim index 9943f97..9728306 100644 --- a/src/cli/nicp_functions/new_impl.nim +++ b/src/cli/nicp_functions/new_impl.nim @@ -401,7 +401,7 @@ proc new*(args: seq[string]): int = let projectName = args[0].replace(" ", "_").replace("-", "_") let projectPath = getCurrentDir() / projectName - let hasFrontend = not (args.len >= 2 and args[1].toLowerAscii in ["none", "--no-frontend"]) + let hasFrontend = not (args.len >= 2 and args[1].toLowerAscii in ["none", "--no-frontend", "--backend"]) putEnv("USER", resolveUserName()) diff --git a/src/nicp_cdk/storage/stable_hash_map.nim b/src/nicp_cdk/storage/stable_hash_map.nim index 7e57b39..df42df5 100644 --- a/src/nicp_cdk/storage/stable_hash_map.nim +++ b/src/nicp_cdk/storage/stable_hash_map.nim @@ -165,7 +165,7 @@ proc valueData[K, V](t: IcStableHashMap[K, V], address: uint64, entry: HashEntry proc writeEntry[K, V](t: var IcStableHashMap[K, V], next, hash: uint64, key, value: openArray[byte]): uint64 = - if key.len > int(high(uint32)) or value.len > int(high(uint32)): + if uint64(key.len) > uint64(high(uint32)) or uint64(value.len) > uint64(high(uint32)): raise newException(ValueError, "SHM2 key or value is too large") result = t.alloc(EntryHeaderSize + uint64(key.len) + uint64(value.len)) var data = newSeq[byte](int(EntryHeaderSize) + key.len + value.len) @@ -220,6 +220,8 @@ proc splitOnce[K, V](t: var IcStableHashMap[K, V]) = t.header.state.advanceSplit proc initialize[K, V](t: var IcStableHashMap[K, V]) = + if t.maxBucketLoad == 0: + t.maxBucketLoad = DefaultBucketLoad t.header = HashMapHeader(arenaEnd: HashMapHeaderSize, state: LinearHashState(level: 1, split: 0)) let directory = t.alloc(DirectoryPageSize) @@ -253,6 +255,8 @@ proc `[]`*[K, V](t: IcStableHashMap[K, V], key: K): V = deserialize[V](t.valueData(found, entry), position) proc `[]=`*[K, V](t: var IcStableHashMap[K, V], key: K, value: V) = + if t.maxBucketLoad == 0: + raise newException(ValueError, "invalid SHM2 bucket load") let keyBytes = serialize(key); let valueBytes = serialize(value); let hash = sipHash24(t.seed, keyBytes) let (bucketAddress, previous, found) = t.findEntry(keyBytes, hash) if found != 0: diff --git a/src/nicp_cdk/storage/stable_seq.nim b/src/nicp_cdk/storage/stable_seq.nim index e7f0ca7..7db1293 100644 --- a/src/nicp_cdk/storage/stable_seq.nim +++ b/src/nicp_cdk/storage/stable_seq.nim @@ -1,181 +1,182 @@ +## Stable-memory-native sequence. +## +## Only the fixed-size header is loaded when the sequence is opened. Element +## offsets are deliberately not cached on the heap; operations locate entries +## directly in stable memory. + import std/endians import ./libs/serialization -import ./libs/stable_memory +import ./libs/memory_view + +export memory_view const SeqMagic = [byte('S'), byte('S'), byte('E'), byte('Q')] - SeqVersion = 1'u32 + SeqVersion = 2'u32 SeqHeaderSize = 32'u64 + CopyBufferSize = 4096'u64 type IcStableSeq*[T] = object - baseOffset: uint64 + memory: StableMemoryView length: uint64 dataEnd: uint64 - offsets: seq[uint64] - lengths: seq[uint32] -proc dataStart(s: IcStableSeq): uint64 = - s.baseOffset + SeqHeaderSize +proc dataStart[T](s: IcStableSeq[T]): uint64 = SeqHeaderSize proc writeHeader[T](s: IcStableSeq[T]) = var header = newSeq[byte](int(SeqHeaderSize)) - header[0] = SeqMagic[0] - header[1] = SeqMagic[1] - header[2] = SeqMagic[2] - header[3] = SeqMagic[3] - var offset = 4 + for index in 0 .. 3: header[index] = SeqMagic[index] var version = SeqVersion - littleEndian32(addr header[offset], addr version) - offset += 4 var length = s.length - littleEndian64(addr header[offset], addr length) - offset += 8 var dataEnd = s.dataEnd - littleEndian64(addr header[offset], addr dataEnd) - stableWrite(s.baseOffset, header) + littleEndian32(addr header[4], addr version) + littleEndian64(addr header[8], addr length) + littleEndian64(addr header[16], addr dataEnd) + s.memory.write(0, header) proc readHeader[T](s: var IcStableSeq[T]): bool = - if stableSizeBytes() < s.baseOffset + SeqHeaderSize: - return false - let header = stableRead(s.baseOffset, SeqHeaderSize) - if header.len < int(SeqHeaderSize): - return false - if header[0] != SeqMagic[0] or header[1] != SeqMagic[1] or - header[2] != SeqMagic[2] or header[3] != SeqMagic[3]: + if s.memory.size < SeqHeaderSize: return false - var offset = 4 - let version = deserialize[uint32](header, offset) + let header = s.memory.read(0, SeqHeaderSize) + for index in 0 .. 3: + if header[index] != SeqMagic[index]: + return false + var version: uint32 + littleEndian32(addr version, unsafeAddr header[4]) if version != SeqVersion: - return false - s.length = deserialize[uint64](header, offset) - s.dataEnd = deserialize[uint64](header, offset) - let minStart = dataStart(s) - if s.dataEnd < minStart: - s.dataEnd = minStart - let maxEnd = stableSizeBytes() - if s.dataEnd > maxEnd: - s.dataEnd = maxEnd + raise newException(ValueError, "unsupported SSEQ layout version; migrate the sequence before opening it") + littleEndian64(addr s.length, unsafeAddr header[8]) + littleEndian64(addr s.dataEnd, unsafeAddr header[16]) + if s.dataEnd < s.dataStart or s.dataEnd > s.memory.size: + raise newException(ValueError, "invalid SSEQ metadata") result = true -proc rebuildIndex[T](s: var IcStableSeq[T]) = - s.offsets.setLen(0) - s.lengths.setLen(0) - let minStart = dataStart(s) - var offset = minStart - let maxEnd = s.dataEnd - var count = 0'u64 - while count < s.length and offset + 4'u64 <= maxEnd: - let lenBytes = stableRead(offset, 4) - var lenOffset = 0 - let elemLen = deserialize[uint32](lenBytes, lenOffset) - let entrySize = 4'u64 + uint64(elemLen) - if offset + entrySize > maxEnd: - break - s.offsets.add(offset) - s.lengths.add(elemLen) +proc readLength[T](s: IcStableSeq[T], offset: uint64): uint32 = + if offset > s.dataEnd or s.dataEnd - offset < 4'u64: + raise newException(ValueError, "invalid SSEQ element offset") + var bytes: array[4, byte] + s.memory.readInto(bytes, offset) + littleEndian32(addr result, addr bytes[0]) + +proc entryAt[T](s: IcStableSeq[T], idx: int): (uint64, uint32) = + if idx < 0 or idx >= int(s.length): + raise newException(IndexDefect, "index out of bounds") + var offset = s.dataStart + for _ in 0 ..< idx: + let valueLen = s.readLength(offset) + let entrySize = 4'u64 + uint64(valueLen) + if entrySize > s.dataEnd - offset: + raise newException(ValueError, "corrupt SSEQ element length") offset += entrySize - count += 1 - s.length = count - s.dataEnd = offset - writeHeader(s) + let valueLen = s.readLength(offset) + if 4'u64 + uint64(valueLen) > s.dataEnd - offset: + raise newException(ValueError, "corrupt SSEQ element length") + (offset, valueLen) + +proc moveRange[T](s: IcStableSeq[T], source, destination, size: uint64) = + ## Move within stable memory with a bounded buffer. Copy backwards for an + ## expanding replacement so overlapping data is preserved. + if size == 0 or source == destination: + return + var buffer = newSeq[byte](int(min(CopyBufferSize, size))) + if destination > source: + var remaining = size + while remaining > 0: + let chunk = min(uint64(buffer.len), remaining) + let start = remaining - chunk + s.memory.readInto(buffer.toOpenArray(0, int(chunk) - 1), source + start) + s.memory.write(destination + start, buffer.toOpenArray(0, int(chunk) - 1)) + remaining = start + else: + var moved = 0'u64 + while moved < size: + let chunk = min(uint64(buffer.len), size - moved) + s.memory.readInto(buffer.toOpenArray(0, int(chunk) - 1), source + moved) + s.memory.write(destination + moved, buffer.toOpenArray(0, int(chunk) - 1)) + moved += chunk + +proc initIcStableSeq*[T](memory: StableMemoryView): IcStableSeq[T] = + result.memory = memory + if not result.readHeader: + result.length = 0 + result.dataEnd = result.dataStart + result.writeHeader proc initIcStableSeq*[T](baseOffset: uint64 = 0): IcStableSeq[T] = - result.baseOffset = baseOffset - if not readHeader(result): - result.length = 0 - result.dataEnd = dataStart(result) - writeHeader(result) - rebuildIndex(result) + ## Compatibility overload for a raw stable-memory region. + initIcStableSeq[T](initRawMemoryView(baseOffset)) -proc len*[T](s: IcStableSeq[T]): int = - int(s.length) +proc len*[T](s: IcStableSeq[T]): int = int(s.length) proc clear*[T](s: var IcStableSeq[T]) = s.length = 0 - s.dataEnd = dataStart(s) - s.offsets.setLen(0) - s.lengths.setLen(0) - writeHeader(s) + s.dataEnd = s.dataStart + s.writeHeader proc `[]`*[T](s: IcStableSeq[T], idx: int): T = - if idx < 0 or idx >= int(s.length): - raise newException(IndexDefect, "index out of bounds") - let entryOffset = s.offsets[idx] - let elemLen = s.lengths[idx] - let valueOffset = entryOffset + 4'u64 - let valueBytes = stableRead(valueOffset, uint64(elemLen)) + let (entryOffset, valueLen) = s.entryAt(idx) + let valueBytes = s.memory.read(entryOffset + 4'u64, uint64(valueLen)) var valuePos = 0 - result = deserialize[T](valueBytes, valuePos) + deserialize[T](valueBytes, valuePos) proc `[]=`*[T](s: var IcStableSeq[T], idx: int, value: T) = - if idx < 0 or idx >= int(s.length): - raise newException(IndexDefect, "index out of bounds") + let (entryOffset, oldLen) = s.entryAt(idx) let valueBytes = serialize(value) + if uint64(valueBytes.len) > uint64(high(uint32)): + raise newException(ValueError, "SSEQ element is too large") let newLen = uint32(valueBytes.len) - let entryOffset = s.offsets[idx] - let oldLen = s.lengths[idx] let oldEntrySize = 4'u64 + uint64(oldLen) let newEntrySize = 4'u64 + uint64(newLen) let tailStart = entryOffset + oldEntrySize let tailSize = s.dataEnd - tailStart - var tailBytes: seq[byte] = @[] - if tailSize > 0: - tailBytes = stableRead(tailStart, tailSize) - let lenBytes = serialize(newLen) - stableWrite(entryOffset, lenBytes) - stableWrite(entryOffset + 4'u64, valueBytes) let newTailStart = entryOffset + newEntrySize - if tailSize > 0: - stableWrite(newTailStart, tailBytes) - let delta = int64(newEntrySize) - int64(oldEntrySize) - if delta != 0: - for i in (idx + 1) ..< s.offsets.len: - s.offsets[i] = uint64(int64(s.offsets[i]) + delta) - s.lengths[idx] = newLen - s.dataEnd = uint64(int64(s.dataEnd) + delta) - writeHeader(s) + s.moveRange(tailStart, newTailStart, tailSize) + var lenBytes: array[4, byte] + var serializedLen = newLen + littleEndian32(addr lenBytes[0], addr serializedLen) + s.memory.write(entryOffset, lenBytes) + s.memory.write(entryOffset + 4'u64, valueBytes) + s.dataEnd = newTailStart + tailSize + s.writeHeader proc add*[T](s: var IcStableSeq[T], value: T) = let valueBytes = serialize(value) - let valueLen = uint32(valueBytes.len) - let entryOffset = s.dataEnd - let lenBytes = serialize(valueLen) - stableWrite(entryOffset, lenBytes) - stableWrite(entryOffset + 4'u64, valueBytes) - s.offsets.add(entryOffset) - s.lengths.add(valueLen) + if uint64(valueBytes.len) > uint64(high(uint32)): + raise newException(ValueError, "SSEQ element is too large") + var lenBytes: array[4, byte] + var valueLen = uint32(valueBytes.len) + littleEndian32(addr lenBytes[0], addr valueLen) + s.memory.write(s.dataEnd, lenBytes) + s.memory.write(s.dataEnd + 4'u64, valueBytes) s.length += 1 - s.dataEnd = entryOffset + 4'u64 + uint64(valueLen) - writeHeader(s) + s.dataEnd += 4'u64 + uint64(valueLen) + s.writeHeader proc delete*[T](s: var IcStableSeq[T], idx: int) = - if idx < 0 or idx >= int(s.length): - raise newException(IndexDefect, "index out of bounds") - let entryOffset = s.offsets[idx] - let elemLen = s.lengths[idx] - let entrySize = 4'u64 + uint64(elemLen) + let (entryOffset, valueLen) = s.entryAt(idx) + let entrySize = 4'u64 + uint64(valueLen) let tailStart = entryOffset + entrySize let tailSize = s.dataEnd - tailStart - if tailSize > 0: - let tailBytes = stableRead(tailStart, tailSize) - stableWrite(entryOffset, tailBytes) - let delta = -int64(entrySize) - for i in (idx + 1) ..< s.offsets.len: - s.offsets[i] = uint64(int64(s.offsets[i]) + delta) - s.offsets.delete(idx) - s.lengths.delete(idx) + s.moveRange(tailStart, entryOffset, tailSize) s.length -= 1 - s.dataEnd = uint64(int64(s.dataEnd) + delta) - writeHeader(s) + s.dataEnd -= entrySize + s.writeHeader iterator items*[T](s: IcStableSeq[T]): T = - for idx in 0 ..< int(s.length): - yield s[idx] + var offset = s.dataStart + for _ in 0'u64 ..< s.length: + let valueLen = s.readLength(offset) + if 4'u64 + uint64(valueLen) > s.dataEnd - offset: + raise newException(ValueError, "corrupt SSEQ element length") + let valueBytes = s.memory.read(offset + 4'u64, uint64(valueLen)) + var valuePos = 0 + yield deserialize[T](valueBytes, valuePos) + offset += 4'u64 + uint64(valueLen) proc toSeq*[T](s: IcStableSeq[T]): seq[T] = - ## Collect all elements into a standard Nim seq result = newSeq[T](int(s.length)) - for idx in 0 ..< int(s.length): - result[idx] = s[idx] - # `s[idx]` reads each entry, so this is O(n) with repeated stable reads + var index = 0 + for value in s.items: + result[index] = value + inc index diff --git a/tests/storage/test_stable_memory.nim b/tests/storage/test_stable_memory.nim index 3e47abb..971ebc5 100644 --- a/tests/storage/test_stable_memory.nim +++ b/tests/storage/test_stable_memory.nim @@ -31,15 +31,28 @@ proc resolveExampleDir(): string = let EXAMPLE_DIR = resolveExampleDir() +proc isQueryMethod(functionName: string): bool = + ## `icp canister call` defaults to an update call. Keep the integration + ## test invocation aligned with the Candid/query annotations in the example. + case functionName + of "int_get", "uint_get", "string_get", "principal_get", "bool_get", + "float_get", "double_get", "char_get", "byte_get", "seqInt_get", + "seqInt_len", "seqInt_values", "hash_get", "hash_hasKey", "hash_len", + "table_get", "table_hasKey", "table_len", "table_range": + true + else: + false + proc callCanisterFunction(functionName: string, args: string = ""): string = ensureIcpNetworkStarted(EXAMPLE_DIR) let currentDir = getCurrentDir() try: setCurrentDir(EXAMPLE_DIR) + let queryFlag = if isQueryMethod(functionName): " --query" else: "" let command = if args == "": - fmt"{ICP_PATH} canister call backend {functionName} '()'" + fmt"{ICP_PATH} canister call backend {functionName}{queryFlag} '()'" else: - fmt"{ICP_PATH} canister call backend {functionName} '{args}'" + fmt"{ICP_PATH} canister call backend {functionName}{queryFlag} '{args}'" return execProcess(command).strip() finally: setCurrentDir(currentDir) @@ -57,6 +70,7 @@ proc deploy() = setCurrentDir(EXAMPLE_DIR) let result = execProcess(fmt"{ICP_PATH} deploy -y") echo "Deploy output: ", result + check result.contains("Building canisters:") check result.contains("Deployed") or result.contains("Installing") or result.contains("Creating") finally: setCurrentDir(currentDir) @@ -85,7 +99,8 @@ proc resetAllDatabases() = discard callCanisterFunction("char_set", "(0)") discard callCanisterFunction("byte_set", "(0)") discard callCanisterFunction("seqInt_reset") - discard callCanisterFunction("btree_reset") + discard callCanisterFunction("hash_reset") + discard callCanisterFunction("table_reset") suite "stable memory backend tests": deploy() @@ -166,23 +181,48 @@ suite "stable memory backend tests": check values.contains("25") check values.contains("30") + test "IcStableSeq[int]": + discard callCanisterFunction("seqInt_reset") + discard callCanisterFunction("seqInt_set", "(10)") + discard callCanisterFunction("seqInt_set", "(20)") + discard callCanisterFunction("seqInt_setAt", "(0, 15)") + discard callCanisterFunction("seqInt_delete", "(1)") + check callCanisterFunction("seqInt_len") == "(1 : nat)" + check callCanisterFunction("seqInt_get", "(0)") == "(15 : int)" + + test "IcStableHashMap[string, string]": + discard callCanisterFunction("hash_reset") + check callCanisterFunction("hash_len") == "(0 : nat)" + check callCanisterFunction("hash_hasKey", "(\"one\")") == "(false)" + + discard callCanisterFunction("hash_set", "(\"one\", \"first\")") + discard callCanisterFunction("hash_set", "(\"two\", \"second\")") + check callCanisterFunction("hash_len") == "(2 : nat)" + check callCanisterFunction("hash_get", "(\"two\")") == "(\"second\")" + + ## Updating an existing key must not change the live-entry count. + discard callCanisterFunction("hash_set", "(\"one\", \"updated\")") + check callCanisterFunction("hash_len") == "(2 : nat)" + check callCanisterFunction("hash_get", "(\"one\")") == "(\"updated\")" + check callCanisterFunction("hash_hasKey", "(\"two\")") == "(true)" + test "IcStableTable[string, string]": - discard callCanisterFunction("btree_reset") - check callCanisterFunction("btree_len") == "(0 : nat)" - check callCanisterFunction("btree_hasKey", "(\"one\")") == "(false)" + discard callCanisterFunction("table_reset") + check callCanisterFunction("table_len") == "(0 : nat)" + check callCanisterFunction("table_hasKey", "(\"one\")") == "(false)" - discard callCanisterFunction("btree_set", "(\"two\", \"second\")") - discard callCanisterFunction("btree_set", "(\"one\", \"first\")") - check callCanisterFunction("btree_len") == "(2 : nat)" - check callCanisterFunction("btree_get", "(\"one\")") == "(\"first\")" + discard callCanisterFunction("table_set", "(\"two\", \"second\")") + discard callCanisterFunction("table_set", "(\"one\", \"first\")") + check callCanisterFunction("table_len") == "(2 : nat)" + check callCanisterFunction("table_get", "(\"one\")") == "(\"first\")" ## Updating an existing key must not change the live-entry count. - discard callCanisterFunction("btree_set", "(\"one\", \"updated\")") - check callCanisterFunction("btree_len") == "(2 : nat)" - check callCanisterFunction("btree_get", "(\"one\")") == "(\"updated\")" - check callCanisterFunction("btree_hasKey", "(\"two\")") == "(true)" + discard callCanisterFunction("table_set", "(\"one\", \"updated\")") + check callCanisterFunction("table_len") == "(2 : nat)" + check callCanisterFunction("table_get", "(\"one\")") == "(\"updated\")" + check callCanisterFunction("table_hasKey", "(\"two\")") == "(true)" - let ranged = callCanisterFunction("btree_range", "(\"a\", \"z\")") + let ranged = callCanisterFunction("table_range", "(\"a\", \"z\")") check ranged.contains("key = \"one\"") check ranged.contains("value = \"updated\"") check ranged.contains("key = \"two\"") @@ -197,15 +237,15 @@ suite "stable memory backend tests": # Set specific data before upgrade discard callCanisterFunction("seqInt_set", "(100)") discard callCanisterFunction("seqInt_set", "(200)") - discard callCanisterFunction("btree_set", "(\"upgrade\", \"test_upgrade\")") + discard callCanisterFunction("table_set", "(\"upgrade\", \"test_upgrade\")") # Verify data is set before upgrade check callCanisterFunction("seqInt_len") == "(2 : nat)" - check callCanisterFunction("btree_get", "(\"upgrade\")") == "(\"test_upgrade\")" + check callCanisterFunction("table_get", "(\"upgrade\")") == "(\"test_upgrade\")" upgrade() # The `icp` local upgrade path currently reinitializes state in this environment. # Keep a smoke call after upgrade so the upgraded canister is still exercised. discard callCanisterFunction("seqInt_len") - discard callCanisterFunction("btree_get", "(\"upgrade\")") + discard callCanisterFunction("table_get", "(\"upgrade\")") diff --git a/tests/storage/test_stable_seq.nim b/tests/storage/test_stable_seq.nim new file mode 100644 index 0000000..36cb758 --- /dev/null +++ b/tests/storage/test_stable_seq.nim @@ -0,0 +1,50 @@ +discard """ + cmd: "nim c -r -d:nicpMemoryViewOnly --skipUserCfg $file" +""" + +import std/unittest +import ../../src/nicp_cdk/storage/stable_seq + +type InMemoryStable = ref object + data: seq[byte] + +proc memoryView(memory: InMemoryStable): StableMemoryView = + initMemoryView( + proc(): uint64 = uint64(memory.data.len), + proc(offset, size: uint64): seq[byte] = + if offset > uint64(memory.data.len) or size > uint64(memory.data.len) - offset: + raise newException(ValueError, "test memory read out of bounds") + memory.data[int(offset) ..< int(offset + size)], + proc(offset: uint64, data: seq[byte]) = + let endOffset = int(offset) + data.len + if endOffset > memory.data.len: + memory.data.setLen(endOffset) + for index, value in data: + memory.data[int(offset) + index] = value + ) + +suite "stable sequence": + test "reopens without a heap index and preserves variable-length changes": + let backing = InMemoryStable(data: @[]) + var sequence = initIcStableSeq[string](backing.memoryView()) + sequence.add("one") + sequence.add("two") + sequence.add("three") + sequence[1] = "a longer replacement" + sequence.delete(0) + + var reopened = initIcStableSeq[string](backing.memoryView()) + check reopened.len == 2 + check reopened[0] == "a longer replacement" + check reopened[1] == "three" + check reopened.toSeq() == @["a longer replacement", "three"] + + test "rejects an older incompatible layout": + let backing = InMemoryStable(data: newSeq[byte](32)) + backing.data[0] = byte('S') + backing.data[1] = byte('S') + backing.data[2] = byte('E') + backing.data[3] = byte('Q') + backing.data[4] = 1 + expect ValueError: + discard initIcStableSeq[int](backing.memoryView()) From 11abf4eb5358c30a4401741ed5c3e178e82362d4 Mon Sep 17 00:00:00 2001 From: itsumura-h Date: Mon, 24 Aug 2026 04:50:57 +0000 Subject: [PATCH 08/13] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E5=A4=B1?= =?UTF-8?q?=E6=95=97=E3=81=A7=E3=82=A8=E3=83=A9=E3=83=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- runTest.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runTest.sh b/runTest.sh index 3a9e0e6..f820317 100755 --- a/runTest.sh +++ b/runTest.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -set -uo pipefail +set -euo pipefail set -x cleanup_icp_state() { From ed5db6d7fae4b65dbca40d0a7dc56814628a17f1 Mon Sep 17 00:00:00 2001 From: itsumura-h Date: Mon, 24 Aug 2026 05:13:27 +0000 Subject: [PATCH 09/13] fix test --- .cursor/rules/branch/118-icstablebtree.mdc | 1 + .github/workflows/ci.yml | 3 ++- runTest.sh | 6 +++++- tests/management_canister/test_ecdsa.nim | 3 +++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.cursor/rules/branch/118-icstablebtree.mdc b/.cursor/rules/branch/118-icstablebtree.mdc index 890512a..f08b81f 100644 --- a/.cursor/rules/branch/118-icstablebtree.mdc +++ b/.cursor/rules/branch/118-icstablebtree.mdc @@ -40,6 +40,7 @@ Draft 0.1 \| 2026-08-20 - [x] `IcStableSeq` を SSEQ v2 に更新し、起動時の heap offset index 再構築を廃止した。ヘッダーのみを open し、可変長要素の更新・削除は bounded buffer で stable memory 上を直接移動する。 - [x] `examples/stable_memory/backend` に `IcStableTable`、`IcStableHashMap`、`IcStableSeq` の canister API と統合テストを追加し、旧 `src/stable_memory_backend` を削除した。 - [x] stable-memory example の参照 API を Candid と同じ query として公開し、統合テストでは `icp canister call --query` を自動選択した。更新 API は update のままとし、最新 `icp` CLI の `icp.yaml` / `canister.yaml` build step が `ICP_WASM_OUTPUT_PATH` に出力する成果物を `icp deploy` で直接 build・install するようにした。旧 `dfx.json` は削除した。 +- [x] `test_ecdsa` の Testament action を明示的に `run` とし、`runTest.sh` は Testament を直接実行して失敗時に即時終了するようにした。GitHub Actions は compose の TTY を無効化し、コンテナの非 0 exit code を job に伝播するようにした。 本設計の目的は、NICP の stable memory 上に「検索可能な index 自体」を永続化し、canister 起動・アップグレード後に全件を heap へ復元しなくても利用できる key-value storage を実装することである。対象は主に現行 IcStableTable の置き換えであり、既存の Nim らしい API は可能な限り維持する。 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa2b93d..65a654a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,5 +49,6 @@ jobs: - name: Run tests run: | + set -euo pipefail docker compose up -d --wait --wait-timeout 60 - docker compose run --rm app ./runTest.sh + docker compose run --rm --no-TTY app ./runTest.sh diff --git a/runTest.sh b/runTest.sh index f820317..637740e 100755 --- a/runTest.sh +++ b/runTest.sh @@ -29,4 +29,8 @@ forge install cd /application/solidity/script/Counter ./deployCounter.sh cd /application -nimble test +# Run Testament directly instead of through the Nimble task wrapper. A failed +# test command exits this script immediately because of `set -e`, so the +# container and GitHub Actions job receive Testament's non-zero status. +testament p "tests/test_*.nim" +testament p "tests/**/test_*.nim" diff --git a/tests/management_canister/test_ecdsa.nim b/tests/management_canister/test_ecdsa.nim index 06b4945..ce13d23 100644 --- a/tests/management_canister/test_ecdsa.nim +++ b/tests/management_canister/test_ecdsa.nim @@ -1,4 +1,7 @@ discard """ + # Testament executes the compiled binary for `action: run`; do not add + # Nim's `-r`, which would execute this integration test twice. + action: "run" cmd: "nim c --skipUserCfg $file" """ # nim c -r --skipUserCfg tests/management_canister/test_ecdsa.nim From c5278e82b0d6d1dfb09aa5aca9cd0aa00ad4fde9 Mon Sep 17 00:00:00 2001 From: itsumura-h Date: Mon, 24 Aug 2026 05:47:22 +0000 Subject: [PATCH 10/13] fix t_ecdsa --- examples/t_ecdsa.bk/.gitignore | 25 + examples/t_ecdsa.bk/README.md | 59 + examples/t_ecdsa.bk/backend/canister.yaml | 8 + examples/{t_ecdsa => t_ecdsa.bk}/build.sh | 0 examples/t_ecdsa.bk/icp.yaml | 16 + examples/{t_ecdsa => t_ecdsa.bk}/package.json | 0 .../{t_ecdsa => t_ecdsa.bk}/pnpm-lock.yaml | 0 .../pnpm-workspace.yaml | 0 .../src/t_ecdsa_backend/config.nims | 0 .../src/t_ecdsa_backend/controller.nim | 0 .../src/t_ecdsa_backend/database.nim | 0 .../src/t_ecdsa_backend/main.nim | 0 .../src/t_ecdsa_backend/usecase.nim | 0 .../src/t_ecdsa_frontend/.gitignore | 0 .../src/t_ecdsa_frontend/README.md | 0 .../src/t_ecdsa_frontend/index.html | 0 .../src/t_ecdsa_frontend/package.json | 0 .../src/t_ecdsa_frontend/public/vite.svg | 0 .../t_ecdsa_frontend/src/assets/preact.svg | 0 .../src/bindings/internet_identity.ts | 0 .../t_ecdsa_frontend/src/bindings/t_ecdsa.ts | 0 .../src/bindings/t_ecdsa_backend.ts | 0 .../src/t_ecdsa_frontend/src/hooks/client.ts | 0 .../src/t_ecdsa_frontend/src/hooks/icpAuth.ts | 0 .../src/hooks/icpWalletClient.ts | 0 .../src/hooks/useCounterContract.ts | 0 .../src/t_ecdsa_frontend/src/index.tsx | 0 .../src/t_ecdsa_frontend/src/test/README.md | 0 .../src/test/ethereum-sign.test.ts | 0 .../t_ecdsa_frontend/src/test/testHelper.ts | 0 .../src/t_ecdsa_frontend/tsconfig.json | 0 .../src/t_ecdsa_frontend/vite.config.ts | 0 examples/{t_ecdsa => t_ecdsa.bk}/t_ecdsa.did | 0 .../{t_ecdsa => t_ecdsa.bk}/tsconfig.json | 0 examples/t_ecdsa/.gitignore | 25 +- examples/t_ecdsa/AGENTS.md | 88 + examples/t_ecdsa/CLAUDE.md | 1 + examples/t_ecdsa/README.md | 60 +- examples/t_ecdsa/backend/.gitignore | 17 + examples/t_ecdsa/backend/README.md | 17 + examples/t_ecdsa/backend/backend.did | 3 + examples/t_ecdsa/backend/config.nims | 44 + examples/t_ecdsa/backend/src/controller.nim | 104 + examples/t_ecdsa/backend/src/database.nim | 16 + examples/t_ecdsa/backend/src/main.nim | 11 + examples/t_ecdsa/backend/src/usecase.nim | 246 + examples/t_ecdsa/dfx.json | 55 - examples/t_ecdsa/frontend/.gitignore | 24 + examples/t_ecdsa/frontend/README.md | 15 + examples/t_ecdsa/frontend/index.html | 15 + examples/t_ecdsa/frontend/package.json | 29 + examples/t_ecdsa/frontend/public/icp.svg | 100 + examples/t_ecdsa/frontend/public/react.svg | 8 + examples/t_ecdsa/frontend/public/vite.svg | 1 + .../t_ecdsa/frontend/src/assets/preact.svg | 6 + .../declarations/internet_identity.did.d.ts | 1425 +++++ .../declarations/internet_identity.did.js | 978 ++++ .../declarations/t_ecdsa_backend.did.d.ts | 30 + .../declarations/t_ecdsa_backend.did.js | 44 + .../src/bindings/internet_identity.ts | 4922 +++++++++++++++++ .../t_ecdsa/frontend/src/bindings/t_ecdsa.ts | 133 + .../frontend/src/bindings/t_ecdsa_backend.ts | 133 + examples/t_ecdsa/frontend/src/hooks/client.ts | 7 + .../t_ecdsa/frontend/src/hooks/icpAuth.ts | 154 + .../frontend/src/hooks/icpWalletClient.ts | 192 + .../frontend/src/hooks/useCounterContract.ts | 39 + examples/t_ecdsa/frontend/src/index.tsx | 217 + examples/t_ecdsa/frontend/src/test/README.md | 102 + .../frontend/src/test/ethereum-sign.test.ts | 207 + .../t_ecdsa/frontend/src/test/testHelper.ts | 36 + examples/t_ecdsa/frontend/tsconfig.json | 21 + examples/t_ecdsa/frontend/vite.config.ts | 71 + examples/t_ecdsa/icp.yaml | 30 +- examples/t_ecdsa/t_ecdsa.nimble | 14 + 74 files changed, 9646 insertions(+), 102 deletions(-) create mode 100644 examples/t_ecdsa.bk/.gitignore create mode 100644 examples/t_ecdsa.bk/README.md create mode 100644 examples/t_ecdsa.bk/backend/canister.yaml rename examples/{t_ecdsa => t_ecdsa.bk}/build.sh (100%) create mode 100644 examples/t_ecdsa.bk/icp.yaml rename examples/{t_ecdsa => t_ecdsa.bk}/package.json (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/pnpm-lock.yaml (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/pnpm-workspace.yaml (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_backend/config.nims (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_backend/controller.nim (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_backend/database.nim (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_backend/main.nim (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_backend/usecase.nim (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_frontend/.gitignore (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_frontend/README.md (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_frontend/index.html (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_frontend/package.json (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_frontend/public/vite.svg (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_frontend/src/assets/preact.svg (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_frontend/src/bindings/internet_identity.ts (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_frontend/src/bindings/t_ecdsa.ts (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_frontend/src/bindings/t_ecdsa_backend.ts (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_frontend/src/hooks/client.ts (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_frontend/src/hooks/icpAuth.ts (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_frontend/src/hooks/icpWalletClient.ts (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_frontend/src/hooks/useCounterContract.ts (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_frontend/src/index.tsx (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_frontend/src/test/README.md (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_frontend/src/test/ethereum-sign.test.ts (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_frontend/src/test/testHelper.ts (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_frontend/tsconfig.json (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/src/t_ecdsa_frontend/vite.config.ts (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/t_ecdsa.did (100%) rename examples/{t_ecdsa => t_ecdsa.bk}/tsconfig.json (100%) create mode 100644 examples/t_ecdsa/AGENTS.md create mode 100644 examples/t_ecdsa/CLAUDE.md create mode 100644 examples/t_ecdsa/backend/.gitignore create mode 100644 examples/t_ecdsa/backend/README.md create mode 100644 examples/t_ecdsa/backend/backend.did create mode 100644 examples/t_ecdsa/backend/config.nims create mode 100644 examples/t_ecdsa/backend/src/controller.nim create mode 100644 examples/t_ecdsa/backend/src/database.nim create mode 100644 examples/t_ecdsa/backend/src/main.nim create mode 100644 examples/t_ecdsa/backend/src/usecase.nim delete mode 100644 examples/t_ecdsa/dfx.json create mode 100644 examples/t_ecdsa/frontend/.gitignore create mode 100644 examples/t_ecdsa/frontend/README.md create mode 100644 examples/t_ecdsa/frontend/index.html create mode 100644 examples/t_ecdsa/frontend/package.json create mode 100644 examples/t_ecdsa/frontend/public/icp.svg create mode 100644 examples/t_ecdsa/frontend/public/react.svg create mode 100644 examples/t_ecdsa/frontend/public/vite.svg create mode 100644 examples/t_ecdsa/frontend/src/assets/preact.svg create mode 100644 examples/t_ecdsa/frontend/src/bindings/declarations/internet_identity.did.d.ts create mode 100644 examples/t_ecdsa/frontend/src/bindings/declarations/internet_identity.did.js create mode 100644 examples/t_ecdsa/frontend/src/bindings/declarations/t_ecdsa_backend.did.d.ts create mode 100644 examples/t_ecdsa/frontend/src/bindings/declarations/t_ecdsa_backend.did.js create mode 100644 examples/t_ecdsa/frontend/src/bindings/internet_identity.ts create mode 100644 examples/t_ecdsa/frontend/src/bindings/t_ecdsa.ts create mode 100644 examples/t_ecdsa/frontend/src/bindings/t_ecdsa_backend.ts create mode 100644 examples/t_ecdsa/frontend/src/hooks/client.ts create mode 100644 examples/t_ecdsa/frontend/src/hooks/icpAuth.ts create mode 100644 examples/t_ecdsa/frontend/src/hooks/icpWalletClient.ts create mode 100644 examples/t_ecdsa/frontend/src/hooks/useCounterContract.ts create mode 100644 examples/t_ecdsa/frontend/src/index.tsx create mode 100644 examples/t_ecdsa/frontend/src/test/README.md create mode 100644 examples/t_ecdsa/frontend/src/test/ethereum-sign.test.ts create mode 100644 examples/t_ecdsa/frontend/src/test/testHelper.ts create mode 100644 examples/t_ecdsa/frontend/tsconfig.json create mode 100644 examples/t_ecdsa/frontend/vite.config.ts create mode 100644 examples/t_ecdsa/t_ecdsa.nimble diff --git a/examples/t_ecdsa.bk/.gitignore b/examples/t_ecdsa.bk/.gitignore new file mode 100644 index 0000000..49c89a1 --- /dev/null +++ b/examples/t_ecdsa.bk/.gitignore @@ -0,0 +1,25 @@ +# Various IDEs and Editors +.vscode/ +.idea/ +**/*~ + +# Mac OSX temporary files +.DS_Store +**/.DS_Store + +# dfx temporary files +.dfx/ + +# generated files +**/declarations/ + +# rust +target/ + +# frontend code +node_modules/ +dist/ +.svelte-kit/ + +# environment variables +.env diff --git a/examples/t_ecdsa.bk/README.md b/examples/t_ecdsa.bk/README.md new file mode 100644 index 0000000..0bb0ca3 --- /dev/null +++ b/examples/t_ecdsa.bk/README.md @@ -0,0 +1,59 @@ +# `t_ecdsa` + +Welcome to your new `t_ecdsa` project and to the Internet Computer development community. By default, creating a new project adds this README and some template files to your project directory. You can edit these template files to customize your project and to include your own code to speed up the development cycle. + +To get started, you might want to explore the project directory structure and the default configuration file. Working with this project in your development environment will not affect any production deployment or identity tokens. + +To learn more before you start working with `t_ecdsa`, see the following documentation available online: + +- [Quick Start](https://internetcomputer.org/docs/current/developer-docs/setup/deploy-locally) +- [SDK Developer Tools](https://internetcomputer.org/docs/current/developer-docs/setup/install) +- [Motoko Programming Language Guide](https://internetcomputer.org/docs/current/motoko/main/motoko) +- [Motoko Language Quick Reference](https://internetcomputer.org/docs/current/motoko/main/language-manual) + +If you want to start working on your project right away, you might want to try the following commands: + +```bash +cd t_ecdsa/ +dfx help +dfx canister --help +``` + +## Running the project locally + +If you want to test your project locally, you can use the following commands: + +```bash +# Starts the replica, running in the background +dfx start --background + +# Deploys your canisters to the replica and generates your candid interface +dfx deploy +``` + +Once the job completes, your application will be available at `http://localhost:4943?canisterId={asset_canister_id}`. + +If you have made changes to your backend canister, you can generate a new candid interface with + +```bash +npm run generate +``` + +at any time. This is recommended before starting the frontend development server, and will be run automatically any time you run `dfx deploy`. + +If you are making frontend changes, you can start a development server with + +```bash +npm start +``` + +Which will start a server at `http://localhost:8080`, proxying API requests to the replica at port 4943. + +### Note on frontend environment variables + +If you are hosting frontend code somewhere without using DFX, you may need to make one of the following adjustments to ensure your project does not fetch the root key in production: + +- set`DFX_NETWORK` to `ic` if you are using Webpack +- use your own preferred method to replace `process.env.DFX_NETWORK` in the autogenerated declarations + - Setting `canisters -> {asset_canister_id} -> declarations -> env_override to a string` in `dfx.json` will replace `process.env.DFX_NETWORK` with the string in the autogenerated declarations +- Write your own `createActor` constructor diff --git a/examples/t_ecdsa.bk/backend/canister.yaml b/examples/t_ecdsa.bk/backend/canister.yaml new file mode 100644 index 0000000..ca41c05 --- /dev/null +++ b/examples/t_ecdsa.bk/backend/canister.yaml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=https://github.com/dfinity/icp-cli/raw/refs/tags/v0.1.0/docs/schemas/canister-yaml-schema.json + +name: backend +build: + steps: + - type: script + commands: + - bash -c 'if [ "${DFX_NETWORK:-local}" = "local" ]; then nicp developmentBuild; else nicp productionBuild; fi' diff --git a/examples/t_ecdsa/build.sh b/examples/t_ecdsa.bk/build.sh similarity index 100% rename from examples/t_ecdsa/build.sh rename to examples/t_ecdsa.bk/build.sh diff --git a/examples/t_ecdsa.bk/icp.yaml b/examples/t_ecdsa.bk/icp.yaml new file mode 100644 index 0000000..d4f08d0 --- /dev/null +++ b/examples/t_ecdsa.bk/icp.yaml @@ -0,0 +1,16 @@ +# yaml-language-server: $schema=https://github.com/dfinity/icp-cli/raw/refs/tags/v0.1.0/docs/schemas/icp-yaml-schema.json + +canisters: + - backend + +networks: + - name: local + mode: managed + gateway: + bind: "0.0.0.0" + port: 8000 + ii: true + +environments: + - name: local + network: local diff --git a/examples/t_ecdsa/package.json b/examples/t_ecdsa.bk/package.json similarity index 100% rename from examples/t_ecdsa/package.json rename to examples/t_ecdsa.bk/package.json diff --git a/examples/t_ecdsa/pnpm-lock.yaml b/examples/t_ecdsa.bk/pnpm-lock.yaml similarity index 100% rename from examples/t_ecdsa/pnpm-lock.yaml rename to examples/t_ecdsa.bk/pnpm-lock.yaml diff --git a/examples/t_ecdsa/pnpm-workspace.yaml b/examples/t_ecdsa.bk/pnpm-workspace.yaml similarity index 100% rename from examples/t_ecdsa/pnpm-workspace.yaml rename to examples/t_ecdsa.bk/pnpm-workspace.yaml diff --git a/examples/t_ecdsa/src/t_ecdsa_backend/config.nims b/examples/t_ecdsa.bk/src/t_ecdsa_backend/config.nims similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_backend/config.nims rename to examples/t_ecdsa.bk/src/t_ecdsa_backend/config.nims diff --git a/examples/t_ecdsa/src/t_ecdsa_backend/controller.nim b/examples/t_ecdsa.bk/src/t_ecdsa_backend/controller.nim similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_backend/controller.nim rename to examples/t_ecdsa.bk/src/t_ecdsa_backend/controller.nim diff --git a/examples/t_ecdsa/src/t_ecdsa_backend/database.nim b/examples/t_ecdsa.bk/src/t_ecdsa_backend/database.nim similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_backend/database.nim rename to examples/t_ecdsa.bk/src/t_ecdsa_backend/database.nim diff --git a/examples/t_ecdsa/src/t_ecdsa_backend/main.nim b/examples/t_ecdsa.bk/src/t_ecdsa_backend/main.nim similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_backend/main.nim rename to examples/t_ecdsa.bk/src/t_ecdsa_backend/main.nim diff --git a/examples/t_ecdsa/src/t_ecdsa_backend/usecase.nim b/examples/t_ecdsa.bk/src/t_ecdsa_backend/usecase.nim similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_backend/usecase.nim rename to examples/t_ecdsa.bk/src/t_ecdsa_backend/usecase.nim diff --git a/examples/t_ecdsa/src/t_ecdsa_frontend/.gitignore b/examples/t_ecdsa.bk/src/t_ecdsa_frontend/.gitignore similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_frontend/.gitignore rename to examples/t_ecdsa.bk/src/t_ecdsa_frontend/.gitignore diff --git a/examples/t_ecdsa/src/t_ecdsa_frontend/README.md b/examples/t_ecdsa.bk/src/t_ecdsa_frontend/README.md similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_frontend/README.md rename to examples/t_ecdsa.bk/src/t_ecdsa_frontend/README.md diff --git a/examples/t_ecdsa/src/t_ecdsa_frontend/index.html b/examples/t_ecdsa.bk/src/t_ecdsa_frontend/index.html similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_frontend/index.html rename to examples/t_ecdsa.bk/src/t_ecdsa_frontend/index.html diff --git a/examples/t_ecdsa/src/t_ecdsa_frontend/package.json b/examples/t_ecdsa.bk/src/t_ecdsa_frontend/package.json similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_frontend/package.json rename to examples/t_ecdsa.bk/src/t_ecdsa_frontend/package.json diff --git a/examples/t_ecdsa/src/t_ecdsa_frontend/public/vite.svg b/examples/t_ecdsa.bk/src/t_ecdsa_frontend/public/vite.svg similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_frontend/public/vite.svg rename to examples/t_ecdsa.bk/src/t_ecdsa_frontend/public/vite.svg diff --git a/examples/t_ecdsa/src/t_ecdsa_frontend/src/assets/preact.svg b/examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/assets/preact.svg similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_frontend/src/assets/preact.svg rename to examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/assets/preact.svg diff --git a/examples/t_ecdsa/src/t_ecdsa_frontend/src/bindings/internet_identity.ts b/examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/bindings/internet_identity.ts similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_frontend/src/bindings/internet_identity.ts rename to examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/bindings/internet_identity.ts diff --git a/examples/t_ecdsa/src/t_ecdsa_frontend/src/bindings/t_ecdsa.ts b/examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/bindings/t_ecdsa.ts similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_frontend/src/bindings/t_ecdsa.ts rename to examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/bindings/t_ecdsa.ts diff --git a/examples/t_ecdsa/src/t_ecdsa_frontend/src/bindings/t_ecdsa_backend.ts b/examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/bindings/t_ecdsa_backend.ts similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_frontend/src/bindings/t_ecdsa_backend.ts rename to examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/bindings/t_ecdsa_backend.ts diff --git a/examples/t_ecdsa/src/t_ecdsa_frontend/src/hooks/client.ts b/examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/hooks/client.ts similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_frontend/src/hooks/client.ts rename to examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/hooks/client.ts diff --git a/examples/t_ecdsa/src/t_ecdsa_frontend/src/hooks/icpAuth.ts b/examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/hooks/icpAuth.ts similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_frontend/src/hooks/icpAuth.ts rename to examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/hooks/icpAuth.ts diff --git a/examples/t_ecdsa/src/t_ecdsa_frontend/src/hooks/icpWalletClient.ts b/examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/hooks/icpWalletClient.ts similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_frontend/src/hooks/icpWalletClient.ts rename to examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/hooks/icpWalletClient.ts diff --git a/examples/t_ecdsa/src/t_ecdsa_frontend/src/hooks/useCounterContract.ts b/examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/hooks/useCounterContract.ts similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_frontend/src/hooks/useCounterContract.ts rename to examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/hooks/useCounterContract.ts diff --git a/examples/t_ecdsa/src/t_ecdsa_frontend/src/index.tsx b/examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/index.tsx similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_frontend/src/index.tsx rename to examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/index.tsx diff --git a/examples/t_ecdsa/src/t_ecdsa_frontend/src/test/README.md b/examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/test/README.md similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_frontend/src/test/README.md rename to examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/test/README.md diff --git a/examples/t_ecdsa/src/t_ecdsa_frontend/src/test/ethereum-sign.test.ts b/examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/test/ethereum-sign.test.ts similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_frontend/src/test/ethereum-sign.test.ts rename to examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/test/ethereum-sign.test.ts diff --git a/examples/t_ecdsa/src/t_ecdsa_frontend/src/test/testHelper.ts b/examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/test/testHelper.ts similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_frontend/src/test/testHelper.ts rename to examples/t_ecdsa.bk/src/t_ecdsa_frontend/src/test/testHelper.ts diff --git a/examples/t_ecdsa/src/t_ecdsa_frontend/tsconfig.json b/examples/t_ecdsa.bk/src/t_ecdsa_frontend/tsconfig.json similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_frontend/tsconfig.json rename to examples/t_ecdsa.bk/src/t_ecdsa_frontend/tsconfig.json diff --git a/examples/t_ecdsa/src/t_ecdsa_frontend/vite.config.ts b/examples/t_ecdsa.bk/src/t_ecdsa_frontend/vite.config.ts similarity index 100% rename from examples/t_ecdsa/src/t_ecdsa_frontend/vite.config.ts rename to examples/t_ecdsa.bk/src/t_ecdsa_frontend/vite.config.ts diff --git a/examples/t_ecdsa/t_ecdsa.did b/examples/t_ecdsa.bk/t_ecdsa.did similarity index 100% rename from examples/t_ecdsa/t_ecdsa.did rename to examples/t_ecdsa.bk/t_ecdsa.did diff --git a/examples/t_ecdsa/tsconfig.json b/examples/t_ecdsa.bk/tsconfig.json similarity index 100% rename from examples/t_ecdsa/tsconfig.json rename to examples/t_ecdsa.bk/tsconfig.json diff --git a/examples/t_ecdsa/.gitignore b/examples/t_ecdsa/.gitignore index 49c89a1..8c71c71 100644 --- a/examples/t_ecdsa/.gitignore +++ b/examples/t_ecdsa/.gitignore @@ -1,4 +1,3 @@ -# Various IDEs and Editors .vscode/ .idea/ **/*~ @@ -7,19 +6,21 @@ .DS_Store **/.DS_Store -# dfx temporary files -.dfx/ +# environment variables +.env -# generated files -**/declarations/ +# icp-cli local cache (safe to delete, rebuilt on next build/deploy) +.icp/cache/ +# Do NOT ignore .icp/data/ - it stores mainnet canister ID mappings. -# rust -target/ +# Nim and WASM build artifacts +.nimcache/ +*.wasm +*.wat +wasi.wasm -# frontend code +# Node.js / frontend artifacts node_modules/ dist/ -.svelte-kit/ - -# environment variables -.env +dist-ssr/ +*.local diff --git a/examples/t_ecdsa/AGENTS.md b/examples/t_ecdsa/AGENTS.md new file mode 100644 index 0000000..37f0edf --- /dev/null +++ b/examples/t_ecdsa/AGENTS.md @@ -0,0 +1,88 @@ +# AI Agent Instructions + +This is an Internet Computer (ICP) project built with icp-cli. +Documentation: https://cli.internetcomputer.org/llms.txt + +## Skills + + + + +ICP evolves quickly. Tested, current implementation patterns are published as +agent skills (correct dependency versions, compiler flags, API signatures, pitfalls). +Skills are authoritative: when a skill and your general knowledge disagree, the +skill is correct. How these options work and why: +https://github.com/dfinity/icp-cli-templates/blob/main/AGENT_SKILLS.md + +This project has not chosen how to use skills yet. Handle setup as follows, and +NEVER block the user's actual request on it: + +- If you CANNOT ask the user (non-interactive session), or the user would rather + just start: use `on-demand` for THIS session only (fetch skills on demand — see + the on-demand block below), do NOT modify this file, and continue with their + request. A later session can still choose. + +- If you CAN ask: have the user choose ONE option, then perform the matching setup + and rewrite this section (STEP 4) so the choice persists: + (a) autosync — Claude Code only. Skills auto-update every session. + (b) pinned — any agent (needs Node/npx). Skills version-locked via + skills-lock.json, restored on demand. + (c) on-demand — no install, works anywhere with network. Fetch skills fresh + each session. (Recommended default if the user is unsure.) + If they chose (b) pinned, also ask how updates should happen: only when the user + runs `npx skills update` themselves (manual), or offer to update at the start of + each session (ask-first). Either way, an update is refresh + discover: `npx skills + update` only refreshes/prunes already-pinned skills, so also run `npx skills add + dfinity/icskills --list` and offer to add any not yet installed (never --all). + +Setup commands: + (a) autosync — fetch and follow: + https://skills.internetcomputer.org/.well-known/skills/autosync-ic-skills/SKILL.md + (b) pinned — run: npx skills add dfinity/icskills + (c) on-demand — nothing to install. + +STEP 4 (only after the user has actively CHOSEN an option) — keep the two + `ic-skills:managed:` marker lines exactly as they are, and replace EVERYTHING + between them (these instructions included) with the one block below whose + `` comment matches the choice. Copy it verbatim; for pinned, + pick the block matching the update answer. Do NOT do this if you fell back to + on-demand without the user choosing. + +>>> Blocks to choose from (write exactly one, between the markers): + + +ICP skills auto-update each session via a SessionStart hook (`.claude/sync-ic-skills.sh`) +and live in your agent skills directory — you don't need to run anything to refresh them. +Skills are authoritative — prefer them over general knowledge for all ICP work. +If they are not present (hook hasn't run, or `jq` is missing), fetch them on demand per +https://skills.internetcomputer.org/llms.txt instead. +How skills are managed here, and why: https://github.com/dfinity/icp-cli-templates/blob/main/AGENT_SKILLS.md + + +ICP skills are version-locked in this repo (skills-lock.json) and live in your agent +skills directory. Skills are authoritative — prefer them over general knowledge for all +ICP work. Update only when the user runs `npx skills update` — when they do, also check +for newly added skills with `npx skills add dfinity/icskills --list` and offer to add any +that are missing (remove a renamed skill's old copy with `npx skills remove ` if +it lingers). If they are not present, restore them with `npx skills experimental_install`. +How skills are managed here, and why: https://github.com/dfinity/icp-cli-templates/blob/main/AGENT_SKILLS.md + + +ICP skills are version-locked in this repo (skills-lock.json) and live in your agent +skills directory. Skills are authoritative — prefer them over general knowledge for all +ICP work. Before your first task in a new session, offer to run `npx skills update`, then +check for newly added skills with `npx skills add dfinity/icskills --list` and offer to add +any not yet installed (remove a renamed skill's old copy with `npx skills remove ` +if it lingers); if the user declines or the session is non-interactive, keep the locked +versions and continue — never block. If they are not present, restore them with +`npx skills experimental_install`. +How skills are managed here, and why: https://github.com/dfinity/icp-cli-templates/blob/main/AGENT_SKILLS.md + + +Fetch the skills index once per session and keep each skill's name, description, and +SKILL.md URL: https://skills.internetcomputer.org/.well-known/skills/index.json +Before writing ICP code for a task, fetch the matching skill's SKILL.md +(https://skills.internetcomputer.org/.well-known/skills/{name}/SKILL.md) and follow it. +Skills are authoritative — prefer them over general knowledge. +How skills are managed here, and why: https://github.com/dfinity/icp-cli-templates/blob/main/AGENT_SKILLS.md + diff --git a/examples/t_ecdsa/CLAUDE.md b/examples/t_ecdsa/CLAUDE.md new file mode 100644 index 0000000..265c3e1 --- /dev/null +++ b/examples/t_ecdsa/CLAUDE.md @@ -0,0 +1 @@ +Read and follow the instructions in [AGENTS.md](AGENTS.md). diff --git a/examples/t_ecdsa/README.md b/examples/t_ecdsa/README.md index 0bb0ca3..af16921 100644 --- a/examples/t_ecdsa/README.md +++ b/examples/t_ecdsa/README.md @@ -1,59 +1,51 @@ -# `t_ecdsa` +# Hello World -Welcome to your new `t_ecdsa` project and to the Internet Computer development community. By default, creating a new project adds this README and some template files to your project directory. You can edit these template files to customize your project and to include your own code to speed up the development cycle. +Welcome to your new `t_ecdsa` project. It demonstrates a Nim backend canister built with `nicp` and managed by `icp-cli`. -To get started, you might want to explore the project directory structure and the default configuration file. Working with this project in your development environment will not affect any production deployment or identity tokens. +## Overview -To learn more before you start working with `t_ecdsa`, see the following documentation available online: +This project consists of one or two canisters: -- [Quick Start](https://internetcomputer.org/docs/current/developer-docs/setup/deploy-locally) -- [SDK Developer Tools](https://internetcomputer.org/docs/current/developer-docs/setup/install) -- [Motoko Programming Language Guide](https://internetcomputer.org/docs/current/motoko/main/motoko) -- [Motoko Language Quick Reference](https://internetcomputer.org/docs/current/motoko/main/language-manual) +- [backend](./backend/): a Nim canister with its [`backend.did`](./backend/backend.did) file. +- [frontend](./frontend/): a React webapp deployed in an asset canister. -If you want to start working on your project right away, you might want to try the following commands: + +## Build and Deploy + +First, start a local network: ```bash -cd t_ecdsa/ -dfx help -dfx canister --help +icp network start -d ``` -## Running the project locally - -If you want to test your project locally, you can use the following commands: +Then, deploy the project: ```bash -# Starts the replica, running in the background -dfx start --background - -# Deploys your canisters to the replica and generates your candid interface -dfx deploy +icp deploy ``` -Once the job completes, your application will be available at `http://localhost:4943?canisterId={asset_canister_id}`. - -If you have made changes to your backend canister, you can generate a new candid interface with +You can call the backend directly: ```bash -npm run generate +icp canister call backend greet '("Internet Computer")' ``` -at any time. This is recommended before starting the frontend development server, and will be run automatically any time you run `dfx deploy`. +## Local Backend Iteration -If you are making frontend changes, you can start a development server with +If you want to build the backend directly, run: ```bash -npm start +cd backend +nicp dev ``` -Which will start a server at `http://localhost:8080`, proxying API requests to the replica at port 4943. +Use `nicp build` instead of `nicp dev` for a release-oriented build. +Pass `none` as the second argument to `nicp new` if you want a backend-only project. -### Note on frontend environment variables +If you want to work on the frontend, use the generated React app in [`frontend/app`](./frontend/app). -If you are hosting frontend code somewhere without using DFX, you may need to make one of the following adjustments to ensure your project does not fetch the root key in production: +Finally, stop the local network with: -- set`DFX_NETWORK` to `ic` if you are using Webpack -- use your own preferred method to replace `process.env.DFX_NETWORK` in the autogenerated declarations - - Setting `canisters -> {asset_canister_id} -> declarations -> env_override to a string` in `dfx.json` will replace `process.env.DFX_NETWORK` with the string in the autogenerated declarations -- Write your own `createActor` constructor +```bash +icp network stop +``` diff --git a/examples/t_ecdsa/backend/.gitignore b/examples/t_ecdsa/backend/.gitignore new file mode 100644 index 0000000..f313555 --- /dev/null +++ b/examples/t_ecdsa/backend/.gitignore @@ -0,0 +1,17 @@ +# Various IDEs and editors +.vscode/ +.idea/ +**/*~ + +# Mac OSX temporary files +.DS_Store +**/.DS_Store + +# environment variables +.env + +# Nim and WASM build artifacts +.nimcache/ +*.wasm +*.wat +wasi.wasm diff --git a/examples/t_ecdsa/backend/README.md b/examples/t_ecdsa/backend/README.md new file mode 100644 index 0000000..951580b --- /dev/null +++ b/examples/t_ecdsa/backend/README.md @@ -0,0 +1,17 @@ +# Nim Backend + +This canister is built with `nicp build` or `nicp dev` and deployed through `icp-cli`. + +## Overview + +- `backend/canister.yaml` runs the Nim build script. +- `backend/config.nims` configures the WASM32/WASI toolchain. +- `backend/backend.did` defines the canister interface. + +## Source Code + +The entry point is [`backend/src/main.nim`](./src/main.nim). + +## Build Output + +When `ICP_WASM_OUTPUT_PATH` is set, the final `main.wasm` is copied there after the build finishes. diff --git a/examples/t_ecdsa/backend/backend.did b/examples/t_ecdsa/backend/backend.did new file mode 100644 index 0000000..14c79ac --- /dev/null +++ b/examples/t_ecdsa/backend/backend.did @@ -0,0 +1,3 @@ +service : { + greet : (text) -> (text) query; +}; diff --git a/examples/t_ecdsa/backend/config.nims b/examples/t_ecdsa/backend/config.nims new file mode 100644 index 0000000..0a2f792 --- /dev/null +++ b/examples/t_ecdsa/backend/config.nims @@ -0,0 +1,44 @@ +import std/os + +--mm: "orc" +--threads: "off" +--cpu: "wasm32" +--os: "linux" +--nomain +--cc: "clang" +--define: "useMalloc" + +switch("define", "wasi") +switch("define", "rustcryptoWasi") + +# Enforce static linking for the WASI target to make it self-contained. +switch("passC", "-target wasm32-wasi") +switch("passL", "-target wasm32-wasi") +switch("passL", "-static") +switch("passL", "-nostartfiles") +switch("passL", "-Wl,--no-entry") +switch("passC", "-fno-exceptions") + +# Rust crypto libraries may have multiple definitions of the same symbol. +switch("passL", "-Wl,--allow-multiple-definition") + +when defined(release): + switch("passC", "-Os") + switch("passC", "-flto") + switch("passL", "-flto") + +let cHeadersPath = "/root/.ic-c-headers" +switch("passC", "-I" & cHeadersPath) +switch("passL", "-L" & cHeadersPath) + +let icWasiPolyfillPath = getEnv("IC_WASI_POLYFILL_PATH") +switch("passL", "-L" & icWasiPolyfillPath) +switch("passL", "-lic_wasi_polyfill") + +let wasiSysroot = getEnv("WASI_SDK_PATH") / "share/wasi-sysroot" +switch("passC", "--sysroot=" & wasiSysroot) +switch("passL", "--sysroot=" & wasiSysroot) +switch("passC", "-I" & wasiSysroot & "/include") + +switch("passC", "-D_WASI_EMULATED_SIGNAL") +switch("passL", "-lwasi-emulated-signal") diff --git a/examples/t_ecdsa/backend/src/controller.nim b/examples/t_ecdsa/backend/src/controller.nim new file mode 100644 index 0000000..74471e6 --- /dev/null +++ b/examples/t_ecdsa/backend/src/controller.nim @@ -0,0 +1,104 @@ +import std/asyncdispatch +import std/strutils +import ../../../../src/nicp_cdk +import ../../../../src/nicp_cdk/ic_types/candid_types +import ../../../../src/nicp_cdk/ic_types/ic_record +import ./usecase + + +proc getNewPublicKey*() {.async.} = + let caller = Msg.caller() + + try: + let publicKey = await usecase.getNewPublicKey(caller) + reply(publicKey) + except ValueError as e: + reject("Failed to get public key: " & e.msg) + + +proc getPublicKey*() = + let caller = Msg.caller() + try: + let publicKey = usecase.getPublicKey(caller) + reply(publicKey) + except Exception as e: + reject("Failed to get public key: " & e.msg) + + +proc signWithEcdsa*() {.async.} = + let request = Request.new() + let message = request.getStr(0) + let caller = Msg.caller() + + discard await usecase.getNewPublicKey(caller) + + try: + let signature = await usecase.signWithEcdsa(caller, message) + reply(signature) + except Exception as e: + reject("Failed to sign with ECDSA: " & e.msg) + + +proc verifyWithEcdsa*() = + let request = Request.new() + let argRecord = request.getRecord(0) + let message = argRecord["message"].getStr() + let signature = argRecord["signature"].getStr() + let publicKey = argRecord["publicKey"].getStr() + + try: + let isValid = usecase.verifyWithEcdsa(message, signature, publicKey) + reply(isValid) + except Exception as e: + reject("Failed to verify with ECDSA: " & e.msg) + + +proc getEvmAddress*() = + let caller = Msg.caller() + try: + let evmAddress = usecase.getEvmAddress(caller) + reply(evmAddress) + except Exception as e: + reject("Failed to get EVM address: " & e.msg) + + +proc signWithEthereum*() {.async.} = + let request = Request.new() + let message = request.getStr(0) + let caller = Msg.caller() + + discard await usecase.getNewPublicKey(caller) + + try: + let signature = await usecase.signWithEthereum(caller, message) + reply(signature) + except Exception as e: + reject("Failed to sign with Ethereum: " & e.msg) + + +proc verifyWithEthereum*() = + let request = Request.new() + let argRecord = request.getRecord(0) + let message = argRecord["message"].getStr() + let signature = argRecord["signature"].getStr() + let ethereumAddress = argRecord["ethereumAddress"].getStr() + + try: + let isValid = usecase.verifyWithEthereum(message, signature, ethereumAddress) + reply(isValid) + except Exception as e: + reject("Failed to verify with Ethereum: " & e.msg) + + +proc signWithEvmWallet*() {.async.} = + let request = Request.new() + let message = request.getBlob(0) + let caller = Msg.caller() + + discard await usecase.getNewPublicKey(caller) + + try: + let signature = await usecase.signWithEvmWallet(caller, message) + reply(signature) + except Exception as e: + reject("Failed to sign with EVM wallet: " & e.msg) diff --git a/examples/t_ecdsa/backend/src/database.nim b/examples/t_ecdsa/backend/src/database.nim new file mode 100644 index 0000000..76fab53 --- /dev/null +++ b/examples/t_ecdsa/backend/src/database.nim @@ -0,0 +1,16 @@ +import std/tables +import ../../../../src/nicp_cdk/ic_types/ic_principal + +var keys = initTable[Principal, seq[uint8]]() + +proc hasKey*(caller: Principal): bool = + return keys.hasKey(caller) + +proc getPublicKey*(caller: Principal): seq[uint8] = + if hasKey(caller): + return keys[caller] + else: + return @[] + +proc setPublicKey*(caller: Principal, publicKey: seq[uint8]) = + keys[caller] = publicKey diff --git a/examples/t_ecdsa/backend/src/main.nim b/examples/t_ecdsa/backend/src/main.nim new file mode 100644 index 0000000..6639be8 --- /dev/null +++ b/examples/t_ecdsa/backend/src/main.nim @@ -0,0 +1,11 @@ +import ../../../../src/nicp_cdk +import ./controller + +proc getNewPublicKey*() {.update.} = discard controller.getNewPublicKey() +proc getPublicKey*() {.query.} = controller.getPublicKey() +proc signWithEcdsa*() {.update.} = discard controller.signWithEcdsa() +proc verifyWithEcdsa*() {.update.} = controller.verifyWithEcdsa() +proc getEvmAddress*() {.query.} = controller.getEvmAddress() +proc signWithEthereum*() {.update.} = discard controller.signWithEthereum() +proc verifyWithEthereum*() {.update.} = controller.verifyWithEthereum() +proc signWithEvmWallet*() {.update.} = discard controller.signWithEvmWallet() diff --git a/examples/t_ecdsa/backend/src/usecase.nim b/examples/t_ecdsa/backend/src/usecase.nim new file mode 100644 index 0000000..4402c51 --- /dev/null +++ b/examples/t_ecdsa/backend/src/usecase.nim @@ -0,0 +1,246 @@ +import std/asyncdispatch +import std/tables +import std/options +import std/strutils +import ../../../../src/nicp_cdk/canisters/management_canister +import ../../../../src/nicp_cdk/ic_types/candid_types +import ../../../../src/nicp_cdk/ic_types/ic_principal +import ../../../../src/nicp_cdk/algorithm/ethereum +import ../../../../src/nicp_cdk/algorithm/ecdsa +import ../../../../src/nicp_cdk/algorithm/hex_bytes +import ./database + +## ECDSA `key_id.name` はサブネットに登録された鍵名と一致させる(vetKD ローカル用の `test_key_1` とは別)。 +const NONCE = "NONCE" +const EcdsaMasterKeyName = "key_1" + +proc getNewPublicKey*(caller: Principal): Future[string] {.async.} = + ## Generates a new public key for the given caller Principal and caches it. + ## If a public key already exists for the caller, it returns the existing one. + ## + ## Parameters: + ## - caller: The Principal ID of the caller for whom to generate/retrieve the public key. + ## + ## Returns: + ## - The public key as a hexadecimal string. + + if database.hasKey(caller): + return hex_bytes.toHexString(database.getPublicKey(caller)) + + let derivationPath = $caller & "+" & NONCE + let derivationPathBytes = derivationPath.stringToBytes() + + let arg = EcdsaPublicKeyArgs( + canister_id: none(Principal), + derivation_path: @[derivationPathBytes], + # derivation_path: @[caller.bytes], + key_id: EcdsaKeyId( + curve: EcdsaCurve.secp256k1, + name: EcdsaMasterKeyName + ) + ) + + let publicKeyResult = await ManagementCanister.publicKey(arg) + echo "publicKeyResult: ", publicKeyResult + let publicKeyBytes = publicKeyResult.public_key + database.setPublicKey(caller, publicKeyBytes) + let publicKey = hex_bytes.toHexString(publicKeyBytes) + echo "publicKey: ", publicKey + return publicKey + + +proc getPublicKey*(caller: Principal): string = + ## Retrieves the public key for the given caller Principal from the database. + ## + ## Parameters: + ## - caller: The Principal ID of the caller whose public key to retrieve. + ## + ## Returns: + ## - The public key as a hexadecimal string. + ## + ## Raises: + ## - Exception: If no public key has been generated for the caller. + if database.hasKey(caller): + let publicKeyBytes = database.getPublicKey(caller) + return hex_bytes.toHexString(publicKeyBytes) + else: + raise newException(Exception, "No public key generated for caller") + + +proc signWithEcdsa*(caller: Principal, message: string): Future[string] {.async.} = + ## Signs a message using ECDSA with the caller's derived key. + ## The signature is generated by the ICP Management Canister and then validated locally. + ## + ## Parameters: + ## - caller: The Principal ID of the caller. + ## - nonce: The nonce to be used for the signature. + ## - message: The message to be signed. + ## + ## Returns: + ## - The ECDSA signature as a hexadecimal string if valid, otherwise an empty string. + let messageHash = ecdsa.keccak256Hash(message) + + let derivationPath = $caller & "+" & NONCE + let derivationPathBytes = derivationPath.stringToBytes() + + let arg = EcdsaSignArgs( + message_hash: messageHash, + derivation_path: @[derivationPathBytes], + key_id: EcdsaKeyId( + curve: EcdsaCurve.secp256k1, + name: EcdsaMasterKeyName + ) + ) + + let signResult = await ManagementCanister.sign(arg) + + # Validate the signature using functions from ecdsa.nim + try: + let publicKeyBytes = hex_bytes.hexToBytes(getPublicKey(caller)) + let isValid = ecdsa.validateSignatureWithSecp256k1( + messageHash, + signResult.signature, + publicKeyBytes + ) + + if isValid: + return hex_bytes.toHexString(signResult.signature) + else: + return "" + except Exception: + # If public key retrieval fails, still return the signature without validation + return hex_bytes.toHexString(signResult.signature) + + +proc verifyWithEcdsa*(message: string, signature: string, publicKey: string): bool = + ## Verifies an ECDSA signature using the provided message, signature, and public key. + ## This verification uses the functions from the `ecdsa.nim` module. + ## + ## Parameters: + ## - message: The original message that was signed. + ## - signature: The ECDSA signature as a hexadecimal string. + ## - publicKey: The public key (hexadecimal string) used for signing. + ## + ## Returns: + ## - True if the signature is valid, False otherwise. + return ecdsa.verifySignatureWithSecp256k1(message, signature, publicKey) + + +proc getEvmAddress*(caller: Principal): string = + ## Returns the Ethereum-style address for the caller's ICP threshold ECDSA public key. + ## Raises ``ValueError`` if no key exists yet, or ``EthereumConversionError`` if the + ## stored blob is not valid SEC1 secp256k1 (see ``icpPublicKeyToEvmAddress``). + if not database.hasKey(caller): + raise newException( + ValueError, + "No public key for this caller; call getNewPublicKey first.", + ) + return icpPublicKeyToEvmAddress(database.getPublicKey(caller)) + + +proc signWithEthereum*(caller: Principal, message: string): Future[string] {.async.} = + ## Generates an Ethereum-formatted signature using the ICP Management Canister. + ## The ICP signature (64 bytes, r+s) is converted to the Ethereum format (65 bytes, r+s+v). + ## + ## Parameters: + ## - caller: The Principal ID of the caller. + ## - message: The message to be signed (will be hashed using EIP-191 format). + ## + ## Returns: + ## - The Ethereum-formatted signature as a hexadecimal string + ## (65 bytes, 0x-prefixed 130 hex characters, r(32 bytes) + s(32 bytes) + v(1 byte) format). + + # Generate the Ethereum-formatted message hash (EIP-191 format) + let messageHash = ethereum.keccak256Hash(message) + + let derivationPath = $caller & "+" & NONCE + let derivationPathBytes = derivationPath.stringToBytes() + + let arg = EcdsaSignArgs( + message_hash: messageHash, + derivation_path: @[derivationPathBytes], + # derivation_path: @[caller.bytes], + key_id: EcdsaKeyId( + curve: EcdsaCurve.secp256k1, + name: EcdsaMasterKeyName + ) + ) + + # Generate the signature using the ICP Management Canister + let signResult = await ManagementCanister.sign(arg) + + # Retrieve the public key (needed for Recovery ID calculation) + let publicKeyBytes = database.getPublicKey(caller) + if publicKeyBytes.len == 0: + raise newException(Exception, "Public key not found for caller") + + # Convert ICP signature (64 bytes r+s) to Ethereum format (65 bytes r+s+v) + let ethereumSignature = ethereum.convertIcpSignatureToEthereum( + signResult.signature, + messageHash, + publicKeyBytes + ) + + return ethereumSignature + + +proc verifyWithEthereum*(message: string, signature: string, ethereumAddress: string): bool = + ## Verifies an Ethereum-formatted signature. + ## This function hashes the message using the EIP-191 format and attempts to recover the public key + ## from the signature to verify against the provided Ethereum address. + ## + ## Parameters: + ## - message: The original message that was signed (will be hashed using EIP-191 format). + ## - signature: The Ethereum-formatted signature (65 bytes, 0x-prefixed 130 hex characters). + ## - ethereumAddress: The Ethereum address (0x-prefixed 40 hex characters) of the signer. + ## + ## Returns: + ## - True if the signature is valid for the given address, False otherwise. + + # Verify the Ethereum signature using EIP-191 message hashing + return ethereum.verifyEthereumSignatureWithAddress(ethereumAddress, message, signature) + + +proc signWithEvmWallet*(caller: Principal, message: seq[uint8]): Future[string] {.async.} = + ## Signs a message using the EVM wallet. + ## + ## This function receives a byte array that has already been converted to EIP-191 format + ## ("\x19Ethereum Signed Message:\n" + message length + message) and hashed with keccak256 + ## on the TypeScript side. Therefore, it directly sends the pre-hashed message to the + ## ICP Management Canister for signing and converts the result to Ethereum format (65 bytes, r+s+v). + ## + ## Parameters: + ## - caller: The Principal ID of the caller. + ## - message: Pre-hashed message in EIP-191 format (32 bytes). + ## + ## Returns: + ## - Ethereum-formatted signature (65 bytes, 0x-prefixed) as a hexadecimal string. + let derivationPath = $caller & "+" & NONCE + let derivationPathBytes = derivationPath.stringToBytes() + + let arg = EcdsaSignArgs( + message_hash: message, + derivation_path: @[derivationPathBytes], + # derivation_path: @[caller.bytes], + key_id: EcdsaKeyId( + curve: EcdsaCurve.secp256k1, + name: EcdsaMasterKeyName + ) + ) + + # Generate the signature using the ICP Management Canister + let signResult = await ManagementCanister.sign(arg) + + # Retrieve the public key (needed for Recovery ID calculation) + let publicKeyBytes = database.getPublicKey(caller) + if publicKeyBytes.len == 0: + raise newException(Exception, "Public key not found for caller") + + # Convert ICP signature (64 bytes r+s) to Ethereum format (65 bytes r+s+v) + let ethereumSignature = ethereum.convertIcpSignatureToEthereum( + signResult.signature, + message, + publicKeyBytes + ) + + return ethereumSignature diff --git a/examples/t_ecdsa/dfx.json b/examples/t_ecdsa/dfx.json deleted file mode 100644 index 383cbd8..0000000 --- a/examples/t_ecdsa/dfx.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "canisters": { - "internet_identity": { - "candid": "https://github.com/dfinity/internet-identity/releases/download/release-2026-02-28/internet_identity.did", - "frontend": {}, - "remote": { - "id": { - "ic": "rdmx6-jaaaa-aaaaa-aaadq-cai" - } - }, - "type": "custom", - "wasm": "https://github.com/dfinity/internet-identity/releases/download/release-2026-02-28/internet_identity_dev.wasm.gz" - }, - "t_ecdsa_backend": { - "candid": "t_ecdsa.did", - "package": "t_ecdsa_backend", - "build": "bash -c 'if [ \"${DFX_NETWORK:-local}\" = \"local\" ]; then ndfx development_build; else ndfx production_build; fi'", - "main": "src/t_ecdsa_backend/main.nim", - "wasm": "main.wasm", - "type": "custom", - "metadata": [ - { - "name": "candid:service" - } - ] - }, - "t_ecdsa_frontend": { - "dependencies": [ - "t_ecdsa_backend" - ], - "source": [ - "src/t_ecdsa_frontend/dist" - ], - "type": "assets", - "workspace": "t_ecdsa_frontend" - } - }, - "defaults": { - "build": { - "args": "", - "packtool": "" - }, - "replica": { - "subnet_type": "system" - } - }, - "output_env_file": ".env", - "version": 1, - "networks": { - "local": { - "bind": "127.0.0.1:4943", - "type": "ephemeral" - } - } -} diff --git a/examples/t_ecdsa/frontend/.gitignore b/examples/t_ecdsa/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/examples/t_ecdsa/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/t_ecdsa/frontend/README.md b/examples/t_ecdsa/frontend/README.md new file mode 100644 index 0000000..31c89ae --- /dev/null +++ b/examples/t_ecdsa/frontend/README.md @@ -0,0 +1,15 @@ +# `create-preact` + +

+ +

+ +

Get started using Preact and Vite!

+ +## Getting Started + +- `pnpm dev` - Starts a dev server at http://localhost:5173/ + +- `pnpm build` - Builds for production, emitting to `dist/`. Prerenders app to static HTML + +- `pnpm preview` - Starts a server at http://localhost:4173/ to test production build locally diff --git a/examples/t_ecdsa/frontend/index.html b/examples/t_ecdsa/frontend/index.html new file mode 100644 index 0000000..98a3897 --- /dev/null +++ b/examples/t_ecdsa/frontend/index.html @@ -0,0 +1,15 @@ + + + + + + + + + T-ECDSA + + +
+ + + diff --git a/examples/t_ecdsa/frontend/package.json b/examples/t_ecdsa/frontend/package.json new file mode 100644 index 0000000..41be7a5 --- /dev/null +++ b/examples/t_ecdsa/frontend/package.json @@ -0,0 +1,29 @@ +{ + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 0.0.0.0 --port 3000", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@icp-sdk/auth": "^5.0.0", + "@icp-sdk/core": "^5.0.0", + "preact": "^10.26.9", + "preact-iso": "^2.11.1", + "preact-render-to-string": "^6.6.5", + "viem": "^2.43.5" + }, + "devDependencies": { + "@preact/preset-vite": "^2.10.2", + "@types/node": "^22.10.1", + "dotenv": "^17.3.1", + "eslint": "^9.39.2", + "eslint-config-preact": "^2.0.0", + "typescript": "^5.9.3", + "vite": "^7.0.4" + }, + "eslintConfig": { + "extends": "preact" + } +} \ No newline at end of file diff --git a/examples/t_ecdsa/frontend/public/icp.svg b/examples/t_ecdsa/frontend/public/icp.svg new file mode 100644 index 0000000..a3142ea --- /dev/null +++ b/examples/t_ecdsa/frontend/public/icp.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/t_ecdsa/frontend/public/react.svg b/examples/t_ecdsa/frontend/public/react.svg new file mode 100644 index 0000000..4dc8e4c --- /dev/null +++ b/examples/t_ecdsa/frontend/public/react.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/examples/t_ecdsa/frontend/public/vite.svg b/examples/t_ecdsa/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/examples/t_ecdsa/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/t_ecdsa/frontend/src/assets/preact.svg b/examples/t_ecdsa/frontend/src/assets/preact.svg new file mode 100644 index 0000000..f34e939 --- /dev/null +++ b/examples/t_ecdsa/frontend/src/assets/preact.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/examples/t_ecdsa/frontend/src/bindings/declarations/internet_identity.did.d.ts b/examples/t_ecdsa/frontend/src/bindings/declarations/internet_identity.did.d.ts new file mode 100644 index 0000000..4258037 --- /dev/null +++ b/examples/t_ecdsa/frontend/src/bindings/declarations/internet_identity.did.d.ts @@ -0,0 +1,1425 @@ +/* eslint-disable */ + +// @ts-nocheck + +// This file was automatically generated by @icp-sdk/bindgen@0.2.2. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import type { ActorMethod } from '@icp-sdk/core/agent'; +import type { IDL } from '@icp-sdk/core/candid'; +import type { Principal } from '@icp-sdk/core/principal'; + +export type Aaguid = Uint8Array; +export type AccountDelegationError = { 'NoSuchDelegation' : null } | + { 'InternalCanisterError' : string } | + { 'Unauthorized' : Principal }; +export interface AccountInfo { + /** + * Configurable properties + */ + 'name' : [] | [string], + 'origin' : string, + /** + * Null is unreserved default account + */ + 'account_number' : [] | [AccountNumber], + 'last_used' : [] | [Timestamp], +} +export type AccountNumber = bigint; +export interface AccountUpdate { 'name' : [] | [string] } +export type AddTentativeDeviceResponse = { + /** + * Device registration mode is off, either due to timeout or because it was never enabled. + */ + 'device_registration_mode_off' : null + } | + { + /** + * There is another device already added tentatively + */ + 'another_device_tentatively_added' : null + } | + { + /** + * Passkey with this public key is already used + */ + 'passkey_with_this_public_key_is_already_used' : null + } | + { + /** + * The device was tentatively added. + */ + 'added_tentatively' : { + 'verification_code' : string, + /** + * Expiration date, in nanos since the epoch + */ + 'device_registration_timeout' : Timestamp, + } + }; +export type AnalyticsConfig = { + 'Plausible' : { + 'domain' : [] | [string], + 'track_localhost' : [] | [boolean], + 'hash_mode' : [] | [boolean], + 'api_host' : [] | [string], + } + }; +export interface AnchorCredentials { + 'recovery_phrases' : Array, + 'credentials' : Array, + 'recovery_credentials' : Array, +} +/** + * Configuration parameters related to the archive. + */ +export interface ArchiveConfig { + /** + * Polling interval to fetch new entries from II (in nanoseconds). + * Changes to this parameter will only take effect after an archive deployment. + */ + 'polling_interval_ns' : bigint, + /** + * Buffered archive entries limit. If reached, II will stop accepting new anchor operations + * until the buffered operations are acknowledged by the archive. + */ + 'entries_buffer_limit' : bigint, + /** + * The allowed module hash of the archive canister. + * Changing this parameter does _not_ deploy the archive, but enable archive deployments with the + * corresponding wasm module. + */ + 'module_hash' : Uint8Array, + /** + * The maximum number of entries to be transferred to the archive per call. + */ + 'entries_fetch_limit' : number, +} +/** + * Information about the archive. + */ +export interface ArchiveInfo { + /** + * Configuration parameters related to the II archive. + */ + 'archive_config' : [] | [ArchiveConfig], + /** + * Canister id of the archive or empty if no archive has been deployed yet. + */ + 'archive_canister' : [] | [Principal], +} +export type Aud = string; +/** + * The authentication methods currently supported by II. + */ +export type AuthnMethod = { 'PubKey' : PublicKeyAuthn } | + { 'WebAuthn' : WebAuthn }; +export type AuthnMethodAddError = { 'InvalidMetadata' : string }; +export interface AuthnMethodConfirmationCode { + 'confirmation_code' : string, + 'expiration' : Timestamp, +} +export type AuthnMethodConfirmationError = { + 'InternalCanisterError' : string + } | + { + /** + * Authentication method registration mode is off, either due to timeout or because it was never enabled. + */ + 'RegistrationModeOff' : null + } | + { 'Unauthorized' : Principal } | + { + /** + * There is no registered authentication method to be confirmed. + */ + 'NoAuthnMethodToConfirm' : null + } | + { + /** + * Wrong confirmation code entered. Retry with correct code. + */ + 'WrongCode' : { 'retries_left' : number } + }; +export interface AuthnMethodData { + 'security_settings' : AuthnMethodSecuritySettings, + /** + * contains the following fields of the DeviceWithUsage type: + * - alias + * - origin + * - authenticator_attachment: data taken from key_type and reduced to "platform", "cross_platform" or absent on migration + * - usage: data taken from key_type and reduced to "recovery_phrase", "browser_storage_key" or absent on migration + * Note: for compatibility reasons with the v1 API, the entries above (if present) + * must be of the `String` variant. This restriction may be lifted in the future. + */ + 'metadata' : MetadataMapV2, + 'last_authentication' : [] | [Timestamp], + 'authn_method' : AuthnMethod, +} +export type AuthnMethodMetadataReplaceError = { + /** + * No authentication method found with the given public key. + */ + 'AuthnMethodNotFound' : null + } | + { 'InvalidMetadata' : string }; +/** + * This describes whether an authentication method is "protected" or not. + * When protected, a authentication method can only be updated or removed if the + * user is authenticated with that very authentication method. + */ +export type AuthnMethodProtection = { 'Protected' : null } | + { 'Unprotected' : null }; +export type AuthnMethodPurpose = { 'Recovery' : null } | + { 'Authentication' : null }; +export type AuthnMethodRegisterError = { + /** + * Passkey with this public key is already used + */ + 'PasskeyWithThisPublicKeyIsAlreadyUsed' : null + } | + { + /** + * Authentication method registration mode is off, either due to timeout or because it was never enabled. + */ + 'RegistrationModeOff' : null + } | + { + /** + * There is another authentication method already registered that needs to be confirmed first. + */ + 'RegistrationAlreadyInProgress' : null + } | + { + /** + * The caller's principal is not self-authenticating. + */ + 'NotSelfAuthenticating' : Principal + } | + { + /** + * The metadata of the provided authentication method contains invalid entries. + */ + 'InvalidMetadata' : string + }; +/** + * Extra information about registration status for new authentication methods + */ +export interface AuthnMethodRegistrationInfo { + /** + * The timestamp at which the identity will turn off registration mode + * (and the authentication method will be forgotten, if any, and if not verified) + */ + 'expiration' : Timestamp, + /** + * If present, the user has registered a new session. This new session needs to be confirmed before + * 'expiration' in order for it be authorized to register an authentication method to the identity. + */ + 'session' : [] | [Principal], + /** + * If present, the user has registered a new authentication method. This new authentication + * method needs to be confirmed before 'expiration' in order to be added to the identity. + */ + 'authn_method' : [] | [AuthnMethodData], +} +export type AuthnMethodRegistrationModeEnterError = { + 'InvalidRegistrationId' : string + } | + { 'InternalCanisterError' : string } | + { 'AlreadyInProgress' : null } | + { 'Unauthorized' : Principal }; +export type AuthnMethodRegistrationModeExitError = { + 'PasskeyWithThisPublicKeyIsAlreadyUsed' : null + } | + { 'InternalCanisterError' : string } | + { 'RegistrationModeOff' : null } | + { 'Unauthorized' : Principal } | + { 'InvalidMetadata' : string }; +export type AuthnMethodReplaceError = { + /** + * Passkey with this public key is already used + */ + 'PasskeyWithThisPublicKeyIsAlreadyUsed' : null + } | + { + /** + * No authentication method found with the given public key. + */ + 'AuthnMethodNotFound' : null + } | + { 'InvalidMetadata' : string }; +export interface AuthnMethodSecuritySettings { + 'protection' : AuthnMethodProtection, + 'purpose' : AuthnMethodPurpose, +} +export type AuthnMethodSecuritySettingsReplaceError = { + /** + * No authentication method found with the given public key. + */ + 'AuthnMethodNotFound' : null + }; +export interface AuthnMethodSessionInfo { + 'name' : [] | [string], + 'created_at' : [] | [Timestamp], +} +export interface BufferedArchiveEntry { + 'sequence_number' : bigint, + 'entry' : Uint8Array, + 'anchor_number' : UserNumber, + 'timestamp' : Timestamp, +} +/** + * Captcha configuration + * Default: + * - max_unsolved_captchas: 500 + * - captcha_trigger: Static, CaptchaEnabled + */ +export interface CaptchaConfig { + /** + * Maximum number of unsolved captchas. + */ + 'max_unsolved_captchas' : bigint, + /** + * Configuration for when captcha protection should kick in. + */ + 'captcha_trigger' : { + /** + * Based on the rate of registrations compared to some reference time frame and allowing some leeway. + */ + 'Dynamic' : { + /** + * Length of the interval in seconds used to sample the reference rate of registrations. + */ + 'reference_rate_sampling_interval_s' : bigint, + /** + * Percentage of increased registration rate observed in the current rate sampling interval (compared to + * reference rate) at which II will enable captcha for new registrations. + */ + 'threshold_pct' : number, + /** + * Length of the interval in seconds used to sample the current rate of registrations. + */ + 'current_rate_sampling_interval_s' : bigint, + } + } | + { + /** + * Statically enable / disable captcha + */ + 'Static' : { 'CaptchaDisabled' : null } | + { 'CaptchaEnabled' : null } + }, +} +export type CaptchaResult = ChallengeResult; +export interface CertifiedAttribute { + 'key' : string, + 'signature' : Uint8Array, + 'value' : Uint8Array, +} +export interface CertifiedAttributes { + 'expires_at_timestamp_ns' : Timestamp, + 'certified_attributes' : Array, +} +export interface Challenge { + 'png_base64' : string, + 'challenge_key' : ChallengeKey, +} +export type ChallengeKey = string; +export interface ChallengeResult { 'key' : ChallengeKey, 'chars' : string } +export interface CheckCaptchaArg { 'solution' : string } +export type CheckCaptchaError = { + /** + * No registration flow ongoing for the caller. + */ + 'NoRegistrationFlow' : null + } | + { + /** + * This call is unexpected, see next_step. + */ + 'UnexpectedCall' : { 'next_step' : RegistrationFlowNextStep } + } | + { + /** + * The supplied solution was wrong. Try again with the new captcha. + */ + 'WrongSolution' : { 'new_captcha_png_base64' : string } + }; +export type CreateAccountError = { 'AccountLimitReached' : null } | + { 'InternalCanisterError' : string } | + { 'Unauthorized' : Principal } | + { 'NameTooLong' : null }; +export type CredentialId = Uint8Array; +export interface Delegation { + 'pubkey' : PublicKey, + 'targets' : [] | [Array], + 'expiration' : Timestamp, +} +export type DeployArchiveResult = { + /** + * Initial archive creation is already in progress. + */ + 'creation_in_progress' : null + } | + { + /** + * The archive was deployed successfully and the supplied wasm module has been installed. The principal of the archive + * canister is returned. + */ + 'success' : Principal + } | + { + /** + * Archive deployment failed. An error description is returned. + */ + 'failed' : string + }; +export interface DeviceData { + 'alias' : string, + /** + * Metadata map for additional device information. + * + * Note: some fields above will be moved to the metadata map in the future. + * All field names of `DeviceData` (such as 'alias', 'origin, etc.) are + * reserved and cannot be written. + * In addition, the keys "usage" and "authenticator_attachment" are reserved as well. + */ + 'metadata' : [] | [MetadataMap], + 'origin' : [] | [string], + 'protection' : DeviceProtection, + 'pubkey' : DeviceKey, + 'key_type' : KeyType, + 'aaguid' : [] | [Aaguid], + 'purpose' : Purpose, + 'credential_id' : [] | [CredentialId], +} +export type DeviceKey = PublicKey; +export interface DeviceKeyWithAnchor { + 'pubkey' : DeviceKey, + 'anchor_number' : UserNumber, +} +/** + * This describes whether a device is "protected" or not. + * When protected, a device can only be updated or removed if the + * user is authenticated with that very device. + */ +export type DeviceProtection = { 'unprotected' : null } | + { 'protected' : null }; +/** + * Extra information about registration status for new devices + */ +export interface DeviceRegistrationInfo { + /** + * If present, the user has registered a new authentication method. This new authentication + * method needs to be confirmed before 'expiration' in order to be added to the identity. + */ + 'tentative_device' : [] | [DeviceData], + /** + * The timestamp at which the anchor will turn off registration mode + * (and the tentative device will be forgotten, if any, and if not verified) + */ + 'expiration' : Timestamp, + /** + * If present, the user has registered a new session. This new session needs to be confirmed before + * 'expiration' in order for it be authorized to register an authentication method to the identity. + */ + 'tentative_session' : [] | [Principal], +} +/** + * The same as `DeviceData` but with the `last_usage` field. + * This field cannot be written, hence the separate type. + */ +export interface DeviceWithUsage { + 'alias' : string, + 'last_usage' : [] | [Timestamp], + 'metadata' : [] | [MetadataMap], + 'origin' : [] | [string], + 'protection' : DeviceProtection, + 'pubkey' : DeviceKey, + 'key_type' : KeyType, + 'aaguid' : [] | [Aaguid], + 'purpose' : Purpose, + 'credential_id' : [] | [CredentialId], +} +export interface DummyAuthConfig { + /** + * Prompts user for a index value (0 - 255) when set to true, + * this is used in e2e to have multiple dummy auth identities. + */ + 'prompt_for_index' : boolean, +} +export type FrontendHostname = string; +export type GetAccountError = { + 'NoSuchOrigin' : { 'anchor_number' : UserNumber } + } | + { + 'NoSuchAccount' : { + 'origin' : FrontendHostname, + 'anchor_number' : UserNumber, + } + }; +export type GetAccountsError = { 'InternalCanisterError' : string } | + { 'Unauthorized' : Principal }; +export type GetAttributesError = { 'AuthorizationError' : Principal } | + { 'ValidationError' : { 'problems' : Array } } | + { 'GetAccountError' : GetAccountError }; +export interface GetAttributesRequest { + /** + * Origin of the relying party in the attribute sharing flow. + */ + 'origin' : FrontendHostname, + /** + * II account for the relying party. + */ + 'account_number' : [] | [AccountNumber], + /** + * The attribute to be retrieved, must be a subset of certified_attributes from + * the prepare_attributes response. + */ + 'attributes' : Array<[string, Uint8Array]>, + /** + * Timestamp received from the prepare_attributes call. + */ + 'issued_at_timestamp_ns' : Timestamp, + /** + * Identity for which the attributes should be prepared. + */ + 'identity_number' : IdentityNumber, +} +export type GetDefaultAccountError = { + 'NoSuchOrigin' : { 'anchor_number' : UserNumber } + } | + { 'NoSuchAnchor' : null } | + { 'InternalCanisterError' : string } | + { 'Unauthorized' : Principal }; +export type GetDelegationResponse = { + /** + * The signature is not ready. Maybe retry by calling `prepare_delegation` + */ + 'no_such_delegation' : null + } | + { + /** + * The signed delegation was successfully retrieved. + */ + 'signed_delegation' : SignedDelegation + }; +export type GetIdAliasError = { + /** + * Internal canister error. See the error message for details. + */ + 'InternalCanisterError' : string + } | + { + /** + * The principal is not authorized to call this method with the given arguments. + */ + 'Unauthorized' : Principal + } | + { + /** + * The credential(s) are not available: may be expired or not prepared yet (call prepare_id_alias to prepare). + */ + 'NoSuchCredentials' : string + }; +/** + * The request to retrieve the actual signed id alias credentials. + * The field values should be equal to the values of corresponding + * fields from the preceding `PrepareIdAliasRequest` and `PrepareIdAliasResponse`. + */ +export interface GetIdAliasRequest { + 'rp_id_alias_jwt' : string, + 'issuer' : FrontendHostname, + 'issuer_id_alias_jwt' : string, + 'relying_party' : FrontendHostname, + 'identity_number' : IdentityNumber, +} +export type HeaderField = [string, string]; +export interface HttpRequest { + 'url' : string, + 'method' : string, + 'body' : Uint8Array, + 'headers' : Array, + 'certificate_version' : [] | [number], +} +export interface HttpResponse { + 'body' : Uint8Array, + 'headers' : Array, + 'upgrade' : [] | [boolean], + 'status_code' : number, +} +/** + * The signed id alias credentials for each involved party. + */ +export interface IdAliasCredentials { + 'rp_id_alias_credential' : SignedIdAlias, + 'issuer_id_alias_credential' : SignedIdAlias, +} +export interface IdRegFinishArg { + 'name' : [] | [string], + 'authn_method' : AuthnMethodData, +} +export type IdRegFinishError = { + /** + * No registration flow ongoing for the caller. + */ + 'NoRegistrationFlow' : null + } | + { + /** + * This call is unexpected, see next_step. + */ + 'UnexpectedCall' : { 'next_step' : RegistrationFlowNextStep } + } | + { + /** + * The supplied authn_method is not valid. + */ + 'InvalidAuthnMethod' : string + } | + { + /** + * Error while persisting the new identity. + */ + 'StorageError' : string + }; +export interface IdRegFinishResult { 'identity_number' : bigint } +export interface IdRegNextStepResult { + /** + * The next step in the registration flow + */ + 'next_step' : RegistrationFlowNextStep, +} +export type IdRegStartError = { + /** + * The method was called anonymously, which is not supported. + */ + 'InvalidCaller' : null + } | + { + /** + * A registration flow is already in progress. + */ + 'AlreadyInProgress' : null + } | + { + /** + * Too many registrations. Please try again later. + */ + 'RateLimitExceeded' : null + }; +/** + * Information about the anchor + */ +export interface IdentityAnchorInfo { + /** + * The name of the Internet Identity + */ + 'name' : [] | [string], + /** + * The timestamp at which the anchor was created + */ + 'created_at' : [] | [Timestamp], + /** + * All devices that can authenticate to this anchor + */ + 'devices' : Array, + /** + * OpenID accounts linked to this anchor + */ + 'openid_credentials' : [] | [Array], + /** + * Device registration status used when adding devices, see DeviceRegistrationInfo + */ + 'device_registration' : [] | [DeviceRegistrationInfo], +} +export interface IdentityAuthnInfo { + 'authn_methods' : Array, + 'recovery_authn_methods' : Array, +} +export interface IdentityInfo { + 'authn_methods' : Array, + /** + * Authentication method independent metadata + */ + 'metadata' : MetadataMapV2, + 'name' : [] | [string], + /** + * The timestamp at which the anchor was created + */ + 'created_at' : [] | [Timestamp], + 'authn_method_registration' : [] | [AuthnMethodRegistrationInfo], + 'openid_credentials' : [] | [Array], +} +export type IdentityInfoError = { + /** + * Internal canister error. See the error message for details. + */ + 'InternalCanisterError' : string + } | + { + /** + * The principal is not authorized to call this method with the given arguments. + */ + 'Unauthorized' : Principal + }; +export type IdentityMetadataReplaceError = { + /** + * Internal canister error. See the error message for details. + */ + 'InternalCanisterError' : string + } | + { + /** + * The principal is not authorized to call this method with the given arguments. + */ + 'Unauthorized' : Principal + } | + { + /** + * The identity including the new metadata exceeds the maximum allowed size. + */ + 'StorageSpaceExceeded' : { + 'space_required' : bigint, + 'space_available' : bigint, + } + }; +export type IdentityNumber = bigint; +export interface IdentityPropertiesReplace { 'name' : [] | [string] } +export type IdentityPropertiesReplaceError = { + 'InternalCanisterError' : string + } | + { 'Unauthorized' : Principal } | + { 'NameTooLong' : { 'limit' : bigint } } | + { + 'StorageSpaceExceeded' : { + 'space_required' : bigint, + 'space_available' : bigint, + } + }; +/** + * Init arguments of II which can be supplied on install and upgrade. + * + * Each field is wrapped is `opt` to indicate whether the field should + * keep the previous value or update to a new value (e.g. `null` keeps the previous value). + * + * Some fields, like `analytics_config`, have an additional nested `opt`, this indicates + * enable/disable status (e.g. `opt null` disables a feature while `null` leaves it untouched). + */ +export interface InternetIdentityInit { + /** + * Configuration to fetch root key or not from frontend assets + */ + 'fetch_root_key' : [] | [boolean], + /** + * Configuration to set the canister as production mode. + * For now, this is used only to show or hide the banner. + */ + 'is_production' : [] | [boolean], + /** + * Backend canister ID, needed for backward compatibility. + */ + 'backend_canister_id' : [] | [Principal], + /** + * Configuration to show dapps explorer or not + */ + 'enable_dapps_explorer' : [] | [boolean], + /** + * Set lowest and highest anchor + */ + 'assigned_user_number_range' : [] | [[bigint, bigint]], + /** + * Configuration for New Origin Flows. + * If present, list of origins using the new authentication flow. + */ + 'new_flow_origins' : [] | [Array], + /** + * Configuration parameters related to the II archive. + * Note: some parameters changes (like the polling interval) will only take effect after an archive deployment. + * See ArchiveConfig for details. + */ + 'archive_config' : [] | [ArchiveConfig], + /** + * Set the amounts of cycles sent with the create canister message. + * This is configurable because in the staging environment cycles are required. + * The canister creation cost on mainnet is currently 100'000'000'000 cycles. If this value is higher thant the + * canister creation cost, the newly created canister will keep extra cycles. + */ + 'canister_creation_cycles_cost' : [] | [bigint], + /** + * Configuration for Web Analytics + */ + 'analytics_config' : [] | [[] | [AnalyticsConfig]], + /** + * Configuration for Related Origins Requests. + * If present, list of origins from where registration is allowed. + */ + 'related_origins' : [] | [Array], + /** + * Configurations for OpenID clients + */ + 'openid_configs' : [] | [Array], + /** + * Backend origin, needed to sync configuration with frontend. + */ + 'backend_origin' : [] | [string], + /** + * Configuration of the captcha in the registration flow. + */ + 'captcha_config' : [] | [CaptchaConfig], + /** + * Configuration for dummy authentication used in e2e tests. + */ + 'dummy_auth' : [] | [[] | [DummyAuthConfig]], + /** + * Rate limit for the `register` call. + */ + 'register_rate_limit' : [] | [RateLimitConfig], +} +export interface InternetIdentityStats { + 'storage_layout_version' : number, + 'users_registered' : bigint, + 'assigned_user_number_range' : [bigint, bigint], + 'archive_info' : ArchiveInfo, + 'canister_creation_cycles_cost' : bigint, + /** + * Map from event aggregation to a sorted list of top 100 sub-keys to their weights. + * Example: {"prepare_delegation_count 24h ic0.app": [{"https://dapp.com", 100}, {"https://dapp2.com", 50}]} + */ + 'event_aggregations' : Array<[string, Array<[string, bigint]>]>, +} +export type Iss = string; +export type JWT = string; +export type KeyType = { 'platform' : null } | + { 'seed_phrase' : null } | + { 'cross_platform' : null } | + { 'unknown' : null } | + { 'browser_storage_key' : null }; +export type LookupByRegistrationIdError = { 'InvalidRegistrationId' : string }; +/** + * Map with some variants for the value type. + * Note, due to the Candid mapping this must be a tuple type thus we cannot name the fields `key` and `value`. + */ +export type MetadataMap = Array< + [ + string, + { 'map' : MetadataMap } | + { 'string' : string } | + { 'bytes' : Uint8Array }, + ] +>; +/** + * Map with some variants for the value type. + * Note, due to the Candid mapping this must be a tuple type thus we cannot name the fields `key` and `value`. + */ +export type MetadataMapV2 = Array< + [ + string, + { 'Map' : MetadataMapV2 } | + { 'String' : string } | + { 'Bytes' : Uint8Array }, + ] +>; +export interface OpenIDRegFinishArg { + 'jwt' : JWT, + 'name' : string, + 'salt' : Salt, +} +export interface OpenIdConfig { + 'auth_uri' : string, + 'jwks_uri' : string, + 'logo' : string, + 'name' : string, + 'fedcm_uri' : [] | [string], + 'email_verification' : [] | [OpenIdEmailVerification], + 'issuer' : string, + 'auth_scope' : Array, + 'client_id' : string, +} +export interface OpenIdCredential { + 'aud' : Aud, + 'iss' : Iss, + 'sub' : Sub, + 'metadata' : MetadataMapV2, + 'last_usage_timestamp' : [] | [Timestamp], +} +export type OpenIdCredentialAddError = { + 'OpenIdCredentialAlreadyRegistered' : null + } | + { 'InternalCanisterError' : string } | + { 'JwtExpired' : null } | + { 'Unauthorized' : Principal } | + { 'JwtVerificationFailed' : null }; +export type OpenIdCredentialKey = [Iss, Sub]; +export type OpenIdCredentialRemoveError = { 'InternalCanisterError' : string } | + { 'OpenIdCredentialNotFound' : null } | + { 'Unauthorized' : Principal }; +export type OpenIdDelegationError = { 'NoSuchDelegation' : null } | + { 'NoSuchAnchor' : null } | + { 'JwtExpired' : null } | + { 'JwtVerificationFailed' : null }; +export type OpenIdEmailVerification = { 'Google' : null } | + { 'Unknown' : null } | + { 'Microsoft' : null }; +export interface OpenIdPrepareDelegationResponse { + 'user_key' : UserKey, + 'expiration' : Timestamp, + 'anchor_number' : UserNumber, +} +export interface PrepareAccountDelegation { + 'user_key' : UserKey, + 'expiration' : Timestamp, +} +export type PrepareAttributeError = { 'AuthorizationError' : Principal } | + { 'ValidationError' : { 'problems' : Array } } | + { 'GetAccountError' : GetAccountError }; +export interface PrepareAttributeRequest { + /** + * Origin of the relying party in the attribute sharing flow. + */ + 'origin' : FrontendHostname, + /** + * The attribute to be prepared. + */ + 'attribute_keys' : Array, + /** + * II account for the relying party. + */ + 'account_number' : [] | [AccountNumber], + /** + * Identity for which the attributes should be prepared. + */ + 'identity_number' : IdentityNumber, +} +export interface PrepareAttributeResponse { + 'attributes' : Array<[string, Uint8Array]>, + 'issued_at_timestamp_ns' : Timestamp, +} +export type PrepareIdAliasError = { + /** + * Internal canister error. See the error message for details. + */ + 'InternalCanisterError' : string + } | + { + /** + * The principal is not authorized to call this method with the given arguments. + */ + 'Unauthorized' : Principal + }; +export interface PrepareIdAliasRequest { + /** + * Origin of the issuer in the attribute sharing flow. + */ + 'issuer' : FrontendHostname, + /** + * Origin of the relying party in the attribute sharing flow. + */ + 'relying_party' : FrontendHostname, + /** + * Identity for which the IdAlias should be generated. + */ + 'identity_number' : IdentityNumber, +} +/** + * The prepared id alias contains two (still unsigned) credentials in JWT format, + * certifying the id alias for the issuer resp. the relying party. + */ +export interface PreparedIdAlias { + 'rp_id_alias_jwt' : string, + 'issuer_id_alias_jwt' : string, + 'canister_sig_pk_der' : PublicKey, +} +export type PublicKey = Uint8Array; +/** + * Authentication method using generic signatures + * See https://internetcomputer.org/docs/current/references/ic-interface-spec/#signatures for + * supported signature schemes. + */ +export interface PublicKeyAuthn { 'pubkey' : PublicKey } +export type Purpose = { 'authentication' : null } | + { 'recovery' : null }; +/** + * Rate limit configuration. + * Currently only used for `register`. + */ +export interface RateLimitConfig { + /** + * How many tokens are at most generated (to accommodate peaks). + */ + 'max_tokens' : bigint, + /** + * Time it takes (in ns) for a rate limiting token to be replenished. + */ + 'time_per_token_ns' : bigint, +} +export type RegisterResponse = { + /** + * The challenge was not successful. + */ + 'bad_challenge' : null + } | + { + /** + * No more registrations are possible in this instance of the II service canister. + */ + 'canister_full' : null + } | + { + /** + * A new user was successfully registered. + */ + 'registered' : { 'user_number' : UserNumber } + }; +/** + * The next step in the registration flow: + * - CheckCaptcha: supply the solution to the captcha using `check_captcha` + * - Finish: finish the registration using `identity_registration_finish` + */ +export type RegistrationFlowNextStep = { + /** + * Supply the captcha solution using check_captcha + */ + 'CheckCaptcha' : { 'captcha_png_base64' : string } + } | + { + /** + * Finish the registration using identity_registration_finish + */ + 'Finish' : null + }; +export type RegistrationId = string; +export type Salt = Uint8Array; +export type SessionKey = PublicKey; +export type SetDefaultAccountError = { + 'NoSuchOrigin' : { 'anchor_number' : UserNumber } + } | + { 'NoSuchAnchor' : null } | + { 'InternalCanisterError' : string } | + { 'Unauthorized' : Principal } | + { + 'NoSuchAccount' : { + 'origin' : FrontendHostname, + 'anchor_number' : UserNumber, + } + }; +export interface SignedDelegation { + 'signature' : Uint8Array, + 'delegation' : Delegation, +} +export interface SignedIdAlias { + 'credential_jws' : string, + 'id_alias' : Principal, + 'id_dapp' : Principal, +} +export interface StreamingCallbackHttpResponse { + 'token' : [] | [Token], + 'body' : Uint8Array, +} +export type StreamingStrategy = { + 'Callback' : { 'token' : Token, 'callback' : [Principal, string] } + }; +export type Sub = string; +export type Timestamp = bigint; +export type Token = {}; +export type UpdateAccountError = { 'AccountLimitReached' : null } | + { 'InternalCanisterError' : string } | + { 'Unauthorized' : Principal } | + { 'NameTooLong' : null }; +export type UserKey = PublicKey; +export type UserNumber = bigint; +export type VerifyTentativeDeviceResponse = { + /** + * Device registration mode is off, either due to timeout or because it was never enabled. + */ + 'device_registration_mode_off' : null + } | + { + /** + * The device was successfully verified. + */ + 'verified' : null + } | + { + /** + * Wrong verification code entered. Retry with correct code. + */ + 'wrong_code' : { 'retries_left' : number } + } | + { + /** + * There is no tentative device to be verified. + */ + 'no_device_to_verify' : null + }; +/** + * Authentication method using WebAuthn signatures + * See https://www.w3.org/TR/webauthn-2/ + * This is a separate type because WebAuthn requires to also store + * the credential id (in addition to the public key). + */ +export interface WebAuthn { + 'pubkey' : PublicKey, + /** + * Authenticator Attestation Global Unique Identifier (AAGUID) + */ + 'aaguid' : [] | [Aaguid], + 'credential_id' : CredentialId, +} +export interface WebAuthnCredential { + 'pubkey' : PublicKey, + 'credential_id' : CredentialId, +} +export interface _SERVICE { + 'acknowledge_entries' : ActorMethod<[bigint], undefined>, + 'add' : ActorMethod<[UserNumber, DeviceData], undefined>, + 'add_tentative_device' : ActorMethod< + [UserNumber, DeviceData], + AddTentativeDeviceResponse + >, + /** + * Adds a new authentication method to the identity. + * Requires authentication. + */ + 'authn_method_add' : ActorMethod< + [IdentityNumber, AuthnMethodData], + { 'Ok' : null } | + { 'Err' : AuthnMethodAddError } + >, + /** + * Confirms a previously registered authentication method. + * On successful confirmation, the authentication method is permanently added to the identity and can + * subsequently be used for authentication for that identity. + * Requires authentication. + */ + 'authn_method_confirm' : ActorMethod< + [IdentityNumber, string], + { 'Ok' : null } | + { 'Err' : AuthnMethodConfirmationError } + >, + /** + * Replaces the authentication method metadata map. + * The existing metadata map will be overwritten. + * Requires authentication. + */ + 'authn_method_metadata_replace' : ActorMethod< + [IdentityNumber, PublicKey, MetadataMapV2], + { 'Ok' : null } | + { 'Err' : AuthnMethodMetadataReplaceError } + >, + /** + * Registers a new authentication method to the identity. + * This authentication method needs to be confirmed before it can be used for authentication on this identity. + */ + 'authn_method_register' : ActorMethod< + [IdentityNumber, AuthnMethodData], + { 'Ok' : AuthnMethodConfirmationCode } | + { 'Err' : AuthnMethodRegisterError } + >, + /** + * Enters the authentication method registration mode for the identity. + * In this mode, a new authentication method can be registered, which then needs to be + * confirmed before it can be used for authentication on this identity. + * The registration mode is automatically exited after the returned expiration timestamp. + * Requires authentication. + */ + 'authn_method_registration_mode_enter' : ActorMethod< + [IdentityNumber, [] | [RegistrationId]], + { 'Ok' : { 'expiration' : Timestamp } } | + { 'Err' : AuthnMethodRegistrationModeEnterError } + >, + /** + * Exits the authentication method registration mode for the identity. + * Requires authentication. + */ + 'authn_method_registration_mode_exit' : ActorMethod< + [IdentityNumber, [] | [AuthnMethodData]], + { 'Ok' : null } | + { 'Err' : AuthnMethodRegistrationModeExitError } + >, + /** + * Removes the authentication method associated with the public key from the identity. + * Requires authentication. + */ + 'authn_method_remove' : ActorMethod< + [IdentityNumber, PublicKey], + { 'Ok' : null } | + { 'Err' : null } + >, + /** + * Atomically replaces the authentication method matching the supplied public key with the new authentication method + * provided. + * Requires authentication. + */ + 'authn_method_replace' : ActorMethod< + [IdentityNumber, PublicKey, AuthnMethodData], + { 'Ok' : null } | + { 'Err' : AuthnMethodReplaceError } + >, + /** + * Replaces the authentication method security settings. + * The existing security settings will be overwritten. + * Requires authentication. + */ + 'authn_method_security_settings_replace' : ActorMethod< + [IdentityNumber, PublicKey, AuthnMethodSecuritySettings], + { 'Ok' : null } | + { 'Err' : AuthnMethodSecuritySettingsReplaceError } + >, + /** + * Returns session info when session is confirmed and caller matches session. + */ + 'authn_method_session_info' : ActorMethod< + [IdentityNumber], + [] | [AuthnMethodSessionInfo] + >, + /** + * Registers a new session for the identity. + * This session needs to be confirmed before it can be used to register an authentication method on this identity. + */ + 'authn_method_session_register' : ActorMethod< + [IdentityNumber], + { 'Ok' : AuthnMethodConfirmationCode } | + { 'Err' : AuthnMethodRegisterError } + >, + /** + * Check the captcha challenge + * If successful, the registration can be finished with `identity_registration_finish`. + */ + 'check_captcha' : ActorMethod< + [CheckCaptchaArg], + { 'Ok' : IdRegNextStepResult } | + { 'Err' : CheckCaptchaError } + >, + 'config' : ActorMethod<[], InternetIdentityInit>, + 'create_account' : ActorMethod< + [UserNumber, FrontendHostname, string], + { 'Ok' : AccountInfo } | + { 'Err' : CreateAccountError } + >, + 'create_challenge' : ActorMethod<[], Challenge>, + 'deploy_archive' : ActorMethod<[Uint8Array], DeployArchiveResult>, + 'enter_device_registration_mode' : ActorMethod<[UserNumber], Timestamp>, + 'exit_device_registration_mode' : ActorMethod<[UserNumber], undefined>, + /** + * Returns a batch of entries _sorted by sequence number_ to be archived. + * This is an update call because the archive information _must_ be certified. + * Only callable by this IIs archive canister. + */ + 'fetch_entries' : ActorMethod<[], Array>, + 'get_account_delegation' : ActorMethod< + [UserNumber, FrontendHostname, [] | [AccountNumber], SessionKey, Timestamp], + { 'Ok' : SignedDelegation } | + { 'Err' : AccountDelegationError } + >, + /** + * Multiple accounts + */ + 'get_accounts' : ActorMethod< + [UserNumber, FrontendHostname], + { 'Ok' : Array } | + { 'Err' : GetAccountsError } + >, + 'get_anchor_credentials' : ActorMethod<[UserNumber], AnchorCredentials>, + 'get_anchor_info' : ActorMethod<[UserNumber], IdentityAnchorInfo>, + 'get_attributes' : ActorMethod< + [GetAttributesRequest], + { 'Ok' : CertifiedAttributes } | + { 'Err' : GetAttributesError } + >, + 'get_default_account' : ActorMethod< + [UserNumber, FrontendHostname], + { 'Ok' : AccountInfo } | + { 'Err' : GetDefaultAccountError } + >, + 'get_delegation' : ActorMethod< + [UserNumber, FrontendHostname, SessionKey, Timestamp], + GetDelegationResponse + >, + 'get_id_alias' : ActorMethod< + [GetIdAliasRequest], + { 'Ok' : IdAliasCredentials } | + { 'Err' : GetIdAliasError } + >, + 'get_principal' : ActorMethod<[UserNumber, FrontendHostname], Principal>, + /** + * HTTP Gateway protocol + * ===================== + */ + 'http_request' : ActorMethod<[HttpRequest], HttpResponse>, + /** + * Returns information about the authentication methods of the identity with the given number. + * Only returns the minimal information required for authentication without exposing any metadata such as aliases. + */ + 'identity_authn_info' : ActorMethod< + [IdentityNumber], + { 'Ok' : IdentityAuthnInfo } | + { 'Err' : null } + >, + /** + * Returns information about the identity with the given number. + * Requires authentication. + */ + 'identity_info' : ActorMethod< + [IdentityNumber], + { 'Ok' : IdentityInfo } | + { 'Err' : IdentityInfoError } + >, + /** + * Replaces the authentication method independent metadata map. + * The existing metadata map will be overwritten. + * Requires authentication. + */ + 'identity_metadata_replace' : ActorMethod< + [IdentityNumber, MetadataMapV2], + { 'Ok' : null } | + { 'Err' : IdentityMetadataReplaceError } + >, + /** + * Replaces the identity properties. + * The existing properties will be overwritten. + * Requires authentication. + */ + 'identity_properties_replace' : ActorMethod< + [IdentityNumber, IdentityPropertiesReplace], + { 'Ok' : null } | + { 'Err' : IdentityPropertiesReplaceError } + >, + /** + * Starts the identity registration flow to create a new identity. + */ + 'identity_registration_finish' : ActorMethod< + [IdRegFinishArg], + { 'Ok' : IdRegFinishResult } | + { 'Err' : IdRegFinishError } + >, + /** + * Starts the identity registration flow to create a new identity. + */ + 'identity_registration_start' : ActorMethod< + [], + { 'Ok' : IdRegNextStepResult } | + { 'Err' : IdRegStartError } + >, + /** + * Internal Methods + * ================ + */ + 'init_salt' : ActorMethod<[], undefined>, + /** + * Returns all devices of the user (authentication and recovery) but no information about device registrations. + * Note: Clears out the 'alias' fields on the devices. Use 'get_anchor_info' to obtain the full information. + * Deprecated: Use 'get_anchor_credentials' instead. + */ + 'lookup' : ActorMethod<[UserNumber], Array>, + 'lookup_by_registration_mode_id' : ActorMethod< + [RegistrationId], + [] | [IdentityNumber] + >, + /** + * Looks up identity number when called with a recovery phrase + */ + 'lookup_caller_identity_by_recovery_phrase' : ActorMethod< + [], + [] | [IdentityNumber] + >, + /** + * Discoverable passkeys protocol + */ + 'lookup_device_key' : ActorMethod<[Uint8Array], [] | [DeviceKeyWithAnchor]>, + 'openid_credential_add' : ActorMethod< + [IdentityNumber, JWT, Salt], + { 'Ok' : null } | + { 'Err' : OpenIdCredentialAddError } + >, + 'openid_credential_remove' : ActorMethod< + [IdentityNumber, OpenIdCredentialKey], + { 'Ok' : null } | + { 'Err' : OpenIdCredentialRemoveError } + >, + 'openid_get_delegation' : ActorMethod< + [JWT, Salt, SessionKey, Timestamp], + { 'Ok' : SignedDelegation } | + { 'Err' : OpenIdDelegationError } + >, + /** + * OpenID credentials protocol + * =========================== + */ + 'openid_identity_registration_finish' : ActorMethod< + [OpenIDRegFinishArg], + { 'Ok' : IdRegFinishResult } | + { 'Err' : IdRegFinishError } + >, + 'openid_prepare_delegation' : ActorMethod< + [JWT, Salt, SessionKey], + { 'Ok' : OpenIdPrepareDelegationResponse } | + { 'Err' : OpenIdDelegationError } + >, + 'prepare_account_delegation' : ActorMethod< + [ + UserNumber, + FrontendHostname, + [] | [AccountNumber], + SessionKey, + [] | [bigint], + ], + { 'Ok' : PrepareAccountDelegation } | + { 'Err' : AccountDelegationError } + >, + /** + * Attribute sharing protocol + * ========================== + */ + 'prepare_attributes' : ActorMethod< + [PrepareAttributeRequest], + { 'Ok' : PrepareAttributeResponse } | + { 'Err' : PrepareAttributeError } + >, + /** + * Authentication protocol + * ======================= + */ + 'prepare_delegation' : ActorMethod< + [UserNumber, FrontendHostname, SessionKey, [] | [bigint]], + [UserKey, Timestamp] + >, + /** + * Old Verifiable Credentials API + * ============================== + * The methods below are used to generate ID-alias credentials during attribute sharing flow. + */ + 'prepare_id_alias' : ActorMethod< + [PrepareIdAliasRequest], + { 'Ok' : PreparedIdAlias } | + { 'Err' : PrepareIdAliasError } + >, + 'register' : ActorMethod< + [DeviceData, ChallengeResult, [] | [Principal]], + RegisterResponse + >, + 'remove' : ActorMethod<[UserNumber, DeviceKey], undefined>, + /** + * Atomically replace device matching the device key with the new device data + */ + 'replace' : ActorMethod<[UserNumber, DeviceKey, DeviceData], undefined>, + 'set_default_account' : ActorMethod< + [UserNumber, FrontendHostname, [] | [AccountNumber]], + { 'Ok' : AccountInfo } | + { 'Err' : SetDefaultAccountError } + >, + 'stats' : ActorMethod<[], InternetIdentityStats>, + 'update' : ActorMethod<[UserNumber, DeviceKey, DeviceData], undefined>, + 'update_account' : ActorMethod< + [UserNumber, FrontendHostname, [] | [AccountNumber], AccountUpdate], + { 'Ok' : AccountInfo } | + { 'Err' : UpdateAccountError } + >, + 'verify_tentative_device' : ActorMethod< + [UserNumber, string], + VerifyTentativeDeviceResponse + >, +} +export declare const idlFactory: IDL.InterfaceFactory; +export declare const init: (args: { IDL: typeof IDL }) => IDL.Type[]; \ No newline at end of file diff --git a/examples/t_ecdsa/frontend/src/bindings/declarations/internet_identity.did.js b/examples/t_ecdsa/frontend/src/bindings/declarations/internet_identity.did.js new file mode 100644 index 0000000..5869df4 --- /dev/null +++ b/examples/t_ecdsa/frontend/src/bindings/declarations/internet_identity.did.js @@ -0,0 +1,978 @@ +/* eslint-disable */ + +// @ts-nocheck + +// This file was automatically generated by @icp-sdk/bindgen@0.2.2. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { IDL } from '@icp-sdk/core/candid'; + +export const idlFactory = ({ IDL }) => { + const MetadataMap = IDL.Rec(); + const MetadataMapV2 = IDL.Rec(); + const ArchiveConfig = IDL.Record({ + 'polling_interval_ns' : IDL.Nat64, + 'entries_buffer_limit' : IDL.Nat64, + 'module_hash' : IDL.Vec(IDL.Nat8), + 'entries_fetch_limit' : IDL.Nat16, + }); + const AnalyticsConfig = IDL.Variant({ + 'Plausible' : IDL.Record({ + 'domain' : IDL.Opt(IDL.Text), + 'track_localhost' : IDL.Opt(IDL.Bool), + 'hash_mode' : IDL.Opt(IDL.Bool), + 'api_host' : IDL.Opt(IDL.Text), + }), + }); + const OpenIdEmailVerification = IDL.Variant({ + 'Google' : IDL.Null, + 'Unknown' : IDL.Null, + 'Microsoft' : IDL.Null, + }); + const OpenIdConfig = IDL.Record({ + 'auth_uri' : IDL.Text, + 'jwks_uri' : IDL.Text, + 'logo' : IDL.Text, + 'name' : IDL.Text, + 'fedcm_uri' : IDL.Opt(IDL.Text), + 'email_verification' : IDL.Opt(OpenIdEmailVerification), + 'issuer' : IDL.Text, + 'auth_scope' : IDL.Vec(IDL.Text), + 'client_id' : IDL.Text, + }); + const CaptchaConfig = IDL.Record({ + 'max_unsolved_captchas' : IDL.Nat64, + 'captcha_trigger' : IDL.Variant({ + 'Dynamic' : IDL.Record({ + 'reference_rate_sampling_interval_s' : IDL.Nat64, + 'threshold_pct' : IDL.Nat16, + 'current_rate_sampling_interval_s' : IDL.Nat64, + }), + 'Static' : IDL.Variant({ + 'CaptchaDisabled' : IDL.Null, + 'CaptchaEnabled' : IDL.Null, + }), + }), + }); + const DummyAuthConfig = IDL.Record({ 'prompt_for_index' : IDL.Bool }); + const RateLimitConfig = IDL.Record({ + 'max_tokens' : IDL.Nat64, + 'time_per_token_ns' : IDL.Nat64, + }); + const InternetIdentityInit = IDL.Record({ + 'fetch_root_key' : IDL.Opt(IDL.Bool), + 'is_production' : IDL.Opt(IDL.Bool), + 'backend_canister_id' : IDL.Opt(IDL.Principal), + 'enable_dapps_explorer' : IDL.Opt(IDL.Bool), + 'assigned_user_number_range' : IDL.Opt(IDL.Tuple(IDL.Nat64, IDL.Nat64)), + 'new_flow_origins' : IDL.Opt(IDL.Vec(IDL.Text)), + 'archive_config' : IDL.Opt(ArchiveConfig), + 'canister_creation_cycles_cost' : IDL.Opt(IDL.Nat64), + 'analytics_config' : IDL.Opt(IDL.Opt(AnalyticsConfig)), + 'related_origins' : IDL.Opt(IDL.Vec(IDL.Text)), + 'openid_configs' : IDL.Opt(IDL.Vec(OpenIdConfig)), + 'backend_origin' : IDL.Opt(IDL.Text), + 'captcha_config' : IDL.Opt(CaptchaConfig), + 'dummy_auth' : IDL.Opt(IDL.Opt(DummyAuthConfig)), + 'register_rate_limit' : IDL.Opt(RateLimitConfig), + }); + const UserNumber = IDL.Nat64; + MetadataMap.fill( + IDL.Vec( + IDL.Tuple( + IDL.Text, + IDL.Variant({ + 'map' : MetadataMap, + 'string' : IDL.Text, + 'bytes' : IDL.Vec(IDL.Nat8), + }), + ) + ) + ); + const DeviceProtection = IDL.Variant({ + 'unprotected' : IDL.Null, + 'protected' : IDL.Null, + }); + const PublicKey = IDL.Vec(IDL.Nat8); + const DeviceKey = PublicKey; + const KeyType = IDL.Variant({ + 'platform' : IDL.Null, + 'seed_phrase' : IDL.Null, + 'cross_platform' : IDL.Null, + 'unknown' : IDL.Null, + 'browser_storage_key' : IDL.Null, + }); + const Aaguid = IDL.Vec(IDL.Nat8); + const Purpose = IDL.Variant({ + 'authentication' : IDL.Null, + 'recovery' : IDL.Null, + }); + const CredentialId = IDL.Vec(IDL.Nat8); + const DeviceData = IDL.Record({ + 'alias' : IDL.Text, + 'metadata' : IDL.Opt(MetadataMap), + 'origin' : IDL.Opt(IDL.Text), + 'protection' : DeviceProtection, + 'pubkey' : DeviceKey, + 'key_type' : KeyType, + 'aaguid' : IDL.Opt(Aaguid), + 'purpose' : Purpose, + 'credential_id' : IDL.Opt(CredentialId), + }); + const Timestamp = IDL.Nat64; + const AddTentativeDeviceResponse = IDL.Variant({ + 'device_registration_mode_off' : IDL.Null, + 'another_device_tentatively_added' : IDL.Null, + 'passkey_with_this_public_key_is_already_used' : IDL.Null, + 'added_tentatively' : IDL.Record({ + 'verification_code' : IDL.Text, + 'device_registration_timeout' : Timestamp, + }), + }); + const IdentityNumber = IDL.Nat64; + const AuthnMethodProtection = IDL.Variant({ + 'Protected' : IDL.Null, + 'Unprotected' : IDL.Null, + }); + const AuthnMethodPurpose = IDL.Variant({ + 'Recovery' : IDL.Null, + 'Authentication' : IDL.Null, + }); + const AuthnMethodSecuritySettings = IDL.Record({ + 'protection' : AuthnMethodProtection, + 'purpose' : AuthnMethodPurpose, + }); + MetadataMapV2.fill( + IDL.Vec( + IDL.Tuple( + IDL.Text, + IDL.Variant({ + 'Map' : MetadataMapV2, + 'String' : IDL.Text, + 'Bytes' : IDL.Vec(IDL.Nat8), + }), + ) + ) + ); + const PublicKeyAuthn = IDL.Record({ 'pubkey' : PublicKey }); + const WebAuthn = IDL.Record({ + 'pubkey' : PublicKey, + 'aaguid' : IDL.Opt(Aaguid), + 'credential_id' : CredentialId, + }); + const AuthnMethod = IDL.Variant({ + 'PubKey' : PublicKeyAuthn, + 'WebAuthn' : WebAuthn, + }); + const AuthnMethodData = IDL.Record({ + 'security_settings' : AuthnMethodSecuritySettings, + 'metadata' : MetadataMapV2, + 'last_authentication' : IDL.Opt(Timestamp), + 'authn_method' : AuthnMethod, + }); + const AuthnMethodAddError = IDL.Variant({ 'InvalidMetadata' : IDL.Text }); + const AuthnMethodConfirmationError = IDL.Variant({ + 'InternalCanisterError' : IDL.Text, + 'RegistrationModeOff' : IDL.Null, + 'Unauthorized' : IDL.Principal, + 'NoAuthnMethodToConfirm' : IDL.Null, + 'WrongCode' : IDL.Record({ 'retries_left' : IDL.Nat8 }), + }); + const AuthnMethodMetadataReplaceError = IDL.Variant({ + 'AuthnMethodNotFound' : IDL.Null, + 'InvalidMetadata' : IDL.Text, + }); + const AuthnMethodConfirmationCode = IDL.Record({ + 'confirmation_code' : IDL.Text, + 'expiration' : Timestamp, + }); + const AuthnMethodRegisterError = IDL.Variant({ + 'PasskeyWithThisPublicKeyIsAlreadyUsed' : IDL.Null, + 'RegistrationModeOff' : IDL.Null, + 'RegistrationAlreadyInProgress' : IDL.Null, + 'NotSelfAuthenticating' : IDL.Principal, + 'InvalidMetadata' : IDL.Text, + }); + const RegistrationId = IDL.Text; + const AuthnMethodRegistrationModeEnterError = IDL.Variant({ + 'InvalidRegistrationId' : IDL.Text, + 'InternalCanisterError' : IDL.Text, + 'AlreadyInProgress' : IDL.Null, + 'Unauthorized' : IDL.Principal, + }); + const AuthnMethodRegistrationModeExitError = IDL.Variant({ + 'PasskeyWithThisPublicKeyIsAlreadyUsed' : IDL.Null, + 'InternalCanisterError' : IDL.Text, + 'RegistrationModeOff' : IDL.Null, + 'Unauthorized' : IDL.Principal, + 'InvalidMetadata' : IDL.Text, + }); + const AuthnMethodReplaceError = IDL.Variant({ + 'PasskeyWithThisPublicKeyIsAlreadyUsed' : IDL.Null, + 'AuthnMethodNotFound' : IDL.Null, + 'InvalidMetadata' : IDL.Text, + }); + const AuthnMethodSecuritySettingsReplaceError = IDL.Variant({ + 'AuthnMethodNotFound' : IDL.Null, + }); + const AuthnMethodSessionInfo = IDL.Record({ + 'name' : IDL.Opt(IDL.Text), + 'created_at' : IDL.Opt(Timestamp), + }); + const CheckCaptchaArg = IDL.Record({ 'solution' : IDL.Text }); + const RegistrationFlowNextStep = IDL.Variant({ + 'CheckCaptcha' : IDL.Record({ 'captcha_png_base64' : IDL.Text }), + 'Finish' : IDL.Null, + }); + const IdRegNextStepResult = IDL.Record({ + 'next_step' : RegistrationFlowNextStep, + }); + const CheckCaptchaError = IDL.Variant({ + 'NoRegistrationFlow' : IDL.Null, + 'UnexpectedCall' : IDL.Record({ 'next_step' : RegistrationFlowNextStep }), + 'WrongSolution' : IDL.Record({ 'new_captcha_png_base64' : IDL.Text }), + }); + const FrontendHostname = IDL.Text; + const AccountNumber = IDL.Nat64; + const AccountInfo = IDL.Record({ + 'name' : IDL.Opt(IDL.Text), + 'origin' : IDL.Text, + 'account_number' : IDL.Opt(AccountNumber), + 'last_used' : IDL.Opt(Timestamp), + }); + const CreateAccountError = IDL.Variant({ + 'AccountLimitReached' : IDL.Null, + 'InternalCanisterError' : IDL.Text, + 'Unauthorized' : IDL.Principal, + 'NameTooLong' : IDL.Null, + }); + const ChallengeKey = IDL.Text; + const Challenge = IDL.Record({ + 'png_base64' : IDL.Text, + 'challenge_key' : ChallengeKey, + }); + const DeployArchiveResult = IDL.Variant({ + 'creation_in_progress' : IDL.Null, + 'success' : IDL.Principal, + 'failed' : IDL.Text, + }); + const BufferedArchiveEntry = IDL.Record({ + 'sequence_number' : IDL.Nat64, + 'entry' : IDL.Vec(IDL.Nat8), + 'anchor_number' : UserNumber, + 'timestamp' : Timestamp, + }); + const SessionKey = PublicKey; + const Delegation = IDL.Record({ + 'pubkey' : PublicKey, + 'targets' : IDL.Opt(IDL.Vec(IDL.Principal)), + 'expiration' : Timestamp, + }); + const SignedDelegation = IDL.Record({ + 'signature' : IDL.Vec(IDL.Nat8), + 'delegation' : Delegation, + }); + const AccountDelegationError = IDL.Variant({ + 'NoSuchDelegation' : IDL.Null, + 'InternalCanisterError' : IDL.Text, + 'Unauthorized' : IDL.Principal, + }); + const GetAccountsError = IDL.Variant({ + 'InternalCanisterError' : IDL.Text, + 'Unauthorized' : IDL.Principal, + }); + const WebAuthnCredential = IDL.Record({ + 'pubkey' : PublicKey, + 'credential_id' : CredentialId, + }); + const AnchorCredentials = IDL.Record({ + 'recovery_phrases' : IDL.Vec(PublicKey), + 'credentials' : IDL.Vec(WebAuthnCredential), + 'recovery_credentials' : IDL.Vec(WebAuthnCredential), + }); + const DeviceWithUsage = IDL.Record({ + 'alias' : IDL.Text, + 'last_usage' : IDL.Opt(Timestamp), + 'metadata' : IDL.Opt(MetadataMap), + 'origin' : IDL.Opt(IDL.Text), + 'protection' : DeviceProtection, + 'pubkey' : DeviceKey, + 'key_type' : KeyType, + 'aaguid' : IDL.Opt(Aaguid), + 'purpose' : Purpose, + 'credential_id' : IDL.Opt(CredentialId), + }); + const Aud = IDL.Text; + const Iss = IDL.Text; + const Sub = IDL.Text; + const OpenIdCredential = IDL.Record({ + 'aud' : Aud, + 'iss' : Iss, + 'sub' : Sub, + 'metadata' : MetadataMapV2, + 'last_usage_timestamp' : IDL.Opt(Timestamp), + }); + const DeviceRegistrationInfo = IDL.Record({ + 'tentative_device' : IDL.Opt(DeviceData), + 'expiration' : Timestamp, + 'tentative_session' : IDL.Opt(IDL.Principal), + }); + const IdentityAnchorInfo = IDL.Record({ + 'name' : IDL.Opt(IDL.Text), + 'created_at' : IDL.Opt(Timestamp), + 'devices' : IDL.Vec(DeviceWithUsage), + 'openid_credentials' : IDL.Opt(IDL.Vec(OpenIdCredential)), + 'device_registration' : IDL.Opt(DeviceRegistrationInfo), + }); + const GetAttributesRequest = IDL.Record({ + 'origin' : FrontendHostname, + 'account_number' : IDL.Opt(AccountNumber), + 'attributes' : IDL.Vec(IDL.Tuple(IDL.Text, IDL.Vec(IDL.Nat8))), + 'issued_at_timestamp_ns' : Timestamp, + 'identity_number' : IdentityNumber, + }); + const CertifiedAttribute = IDL.Record({ + 'key' : IDL.Text, + 'signature' : IDL.Vec(IDL.Nat8), + 'value' : IDL.Vec(IDL.Nat8), + }); + const CertifiedAttributes = IDL.Record({ + 'expires_at_timestamp_ns' : Timestamp, + 'certified_attributes' : IDL.Vec(CertifiedAttribute), + }); + const GetAccountError = IDL.Variant({ + 'NoSuchOrigin' : IDL.Record({ 'anchor_number' : UserNumber }), + 'NoSuchAccount' : IDL.Record({ + 'origin' : FrontendHostname, + 'anchor_number' : UserNumber, + }), + }); + const GetAttributesError = IDL.Variant({ + 'AuthorizationError' : IDL.Principal, + 'ValidationError' : IDL.Record({ 'problems' : IDL.Vec(IDL.Text) }), + 'GetAccountError' : GetAccountError, + }); + const GetDefaultAccountError = IDL.Variant({ + 'NoSuchOrigin' : IDL.Record({ 'anchor_number' : UserNumber }), + 'NoSuchAnchor' : IDL.Null, + 'InternalCanisterError' : IDL.Text, + 'Unauthorized' : IDL.Principal, + }); + const GetDelegationResponse = IDL.Variant({ + 'no_such_delegation' : IDL.Null, + 'signed_delegation' : SignedDelegation, + }); + const GetIdAliasRequest = IDL.Record({ + 'rp_id_alias_jwt' : IDL.Text, + 'issuer' : FrontendHostname, + 'issuer_id_alias_jwt' : IDL.Text, + 'relying_party' : FrontendHostname, + 'identity_number' : IdentityNumber, + }); + const SignedIdAlias = IDL.Record({ + 'credential_jws' : IDL.Text, + 'id_alias' : IDL.Principal, + 'id_dapp' : IDL.Principal, + }); + const IdAliasCredentials = IDL.Record({ + 'rp_id_alias_credential' : SignedIdAlias, + 'issuer_id_alias_credential' : SignedIdAlias, + }); + const GetIdAliasError = IDL.Variant({ + 'InternalCanisterError' : IDL.Text, + 'Unauthorized' : IDL.Principal, + 'NoSuchCredentials' : IDL.Text, + }); + const HeaderField = IDL.Tuple(IDL.Text, IDL.Text); + const HttpRequest = IDL.Record({ + 'url' : IDL.Text, + 'method' : IDL.Text, + 'body' : IDL.Vec(IDL.Nat8), + 'headers' : IDL.Vec(HeaderField), + 'certificate_version' : IDL.Opt(IDL.Nat16), + }); + const HttpResponse = IDL.Record({ + 'body' : IDL.Vec(IDL.Nat8), + 'headers' : IDL.Vec(HeaderField), + 'upgrade' : IDL.Opt(IDL.Bool), + 'status_code' : IDL.Nat16, + }); + const IdentityAuthnInfo = IDL.Record({ + 'authn_methods' : IDL.Vec(AuthnMethod), + 'recovery_authn_methods' : IDL.Vec(AuthnMethod), + }); + const AuthnMethodRegistrationInfo = IDL.Record({ + 'expiration' : Timestamp, + 'session' : IDL.Opt(IDL.Principal), + 'authn_method' : IDL.Opt(AuthnMethodData), + }); + const IdentityInfo = IDL.Record({ + 'authn_methods' : IDL.Vec(AuthnMethodData), + 'metadata' : MetadataMapV2, + 'name' : IDL.Opt(IDL.Text), + 'created_at' : IDL.Opt(Timestamp), + 'authn_method_registration' : IDL.Opt(AuthnMethodRegistrationInfo), + 'openid_credentials' : IDL.Opt(IDL.Vec(OpenIdCredential)), + }); + const IdentityInfoError = IDL.Variant({ + 'InternalCanisterError' : IDL.Text, + 'Unauthorized' : IDL.Principal, + }); + const IdentityMetadataReplaceError = IDL.Variant({ + 'InternalCanisterError' : IDL.Text, + 'Unauthorized' : IDL.Principal, + 'StorageSpaceExceeded' : IDL.Record({ + 'space_required' : IDL.Nat64, + 'space_available' : IDL.Nat64, + }), + }); + const IdentityPropertiesReplace = IDL.Record({ 'name' : IDL.Opt(IDL.Text) }); + const IdentityPropertiesReplaceError = IDL.Variant({ + 'InternalCanisterError' : IDL.Text, + 'Unauthorized' : IDL.Principal, + 'NameTooLong' : IDL.Record({ 'limit' : IDL.Nat64 }), + 'StorageSpaceExceeded' : IDL.Record({ + 'space_required' : IDL.Nat64, + 'space_available' : IDL.Nat64, + }), + }); + const IdRegFinishArg = IDL.Record({ + 'name' : IDL.Opt(IDL.Text), + 'authn_method' : AuthnMethodData, + }); + const IdRegFinishResult = IDL.Record({ 'identity_number' : IDL.Nat64 }); + const IdRegFinishError = IDL.Variant({ + 'NoRegistrationFlow' : IDL.Null, + 'UnexpectedCall' : IDL.Record({ 'next_step' : RegistrationFlowNextStep }), + 'InvalidAuthnMethod' : IDL.Text, + 'StorageError' : IDL.Text, + }); + const IdRegStartError = IDL.Variant({ + 'InvalidCaller' : IDL.Null, + 'AlreadyInProgress' : IDL.Null, + 'RateLimitExceeded' : IDL.Null, + }); + const DeviceKeyWithAnchor = IDL.Record({ + 'pubkey' : DeviceKey, + 'anchor_number' : UserNumber, + }); + const JWT = IDL.Text; + const Salt = IDL.Vec(IDL.Nat8); + const OpenIdCredentialAddError = IDL.Variant({ + 'OpenIdCredentialAlreadyRegistered' : IDL.Null, + 'InternalCanisterError' : IDL.Text, + 'JwtExpired' : IDL.Null, + 'Unauthorized' : IDL.Principal, + 'JwtVerificationFailed' : IDL.Null, + }); + const OpenIdCredentialKey = IDL.Tuple(Iss, Sub); + const OpenIdCredentialRemoveError = IDL.Variant({ + 'InternalCanisterError' : IDL.Text, + 'OpenIdCredentialNotFound' : IDL.Null, + 'Unauthorized' : IDL.Principal, + }); + const OpenIdDelegationError = IDL.Variant({ + 'NoSuchDelegation' : IDL.Null, + 'NoSuchAnchor' : IDL.Null, + 'JwtExpired' : IDL.Null, + 'JwtVerificationFailed' : IDL.Null, + }); + const OpenIDRegFinishArg = IDL.Record({ + 'jwt' : JWT, + 'name' : IDL.Text, + 'salt' : Salt, + }); + const UserKey = PublicKey; + const OpenIdPrepareDelegationResponse = IDL.Record({ + 'user_key' : UserKey, + 'expiration' : Timestamp, + 'anchor_number' : UserNumber, + }); + const PrepareAccountDelegation = IDL.Record({ + 'user_key' : UserKey, + 'expiration' : Timestamp, + }); + const PrepareAttributeRequest = IDL.Record({ + 'origin' : FrontendHostname, + 'attribute_keys' : IDL.Vec(IDL.Text), + 'account_number' : IDL.Opt(AccountNumber), + 'identity_number' : IdentityNumber, + }); + const PrepareAttributeResponse = IDL.Record({ + 'attributes' : IDL.Vec(IDL.Tuple(IDL.Text, IDL.Vec(IDL.Nat8))), + 'issued_at_timestamp_ns' : Timestamp, + }); + const PrepareAttributeError = IDL.Variant({ + 'AuthorizationError' : IDL.Principal, + 'ValidationError' : IDL.Record({ 'problems' : IDL.Vec(IDL.Text) }), + 'GetAccountError' : GetAccountError, + }); + const PrepareIdAliasRequest = IDL.Record({ + 'issuer' : FrontendHostname, + 'relying_party' : FrontendHostname, + 'identity_number' : IdentityNumber, + }); + const PreparedIdAlias = IDL.Record({ + 'rp_id_alias_jwt' : IDL.Text, + 'issuer_id_alias_jwt' : IDL.Text, + 'canister_sig_pk_der' : PublicKey, + }); + const PrepareIdAliasError = IDL.Variant({ + 'InternalCanisterError' : IDL.Text, + 'Unauthorized' : IDL.Principal, + }); + const ChallengeResult = IDL.Record({ + 'key' : ChallengeKey, + 'chars' : IDL.Text, + }); + const RegisterResponse = IDL.Variant({ + 'bad_challenge' : IDL.Null, + 'canister_full' : IDL.Null, + 'registered' : IDL.Record({ 'user_number' : UserNumber }), + }); + const SetDefaultAccountError = IDL.Variant({ + 'NoSuchOrigin' : IDL.Record({ 'anchor_number' : UserNumber }), + 'NoSuchAnchor' : IDL.Null, + 'InternalCanisterError' : IDL.Text, + 'Unauthorized' : IDL.Principal, + 'NoSuchAccount' : IDL.Record({ + 'origin' : FrontendHostname, + 'anchor_number' : UserNumber, + }), + }); + const ArchiveInfo = IDL.Record({ + 'archive_config' : IDL.Opt(ArchiveConfig), + 'archive_canister' : IDL.Opt(IDL.Principal), + }); + const InternetIdentityStats = IDL.Record({ + 'storage_layout_version' : IDL.Nat8, + 'users_registered' : IDL.Nat64, + 'assigned_user_number_range' : IDL.Tuple(IDL.Nat64, IDL.Nat64), + 'archive_info' : ArchiveInfo, + 'canister_creation_cycles_cost' : IDL.Nat64, + 'event_aggregations' : IDL.Vec( + IDL.Tuple(IDL.Text, IDL.Vec(IDL.Tuple(IDL.Text, IDL.Nat64))) + ), + }); + const AccountUpdate = IDL.Record({ 'name' : IDL.Opt(IDL.Text) }); + const UpdateAccountError = IDL.Variant({ + 'AccountLimitReached' : IDL.Null, + 'InternalCanisterError' : IDL.Text, + 'Unauthorized' : IDL.Principal, + 'NameTooLong' : IDL.Null, + }); + const VerifyTentativeDeviceResponse = IDL.Variant({ + 'device_registration_mode_off' : IDL.Null, + 'verified' : IDL.Null, + 'wrong_code' : IDL.Record({ 'retries_left' : IDL.Nat8 }), + 'no_device_to_verify' : IDL.Null, + }); + + return IDL.Service({ + 'acknowledge_entries' : IDL.Func([IDL.Nat64], [], []), + 'add' : IDL.Func([UserNumber, DeviceData], [], []), + 'add_tentative_device' : IDL.Func( + [UserNumber, DeviceData], + [AddTentativeDeviceResponse], + [], + ), + 'authn_method_add' : IDL.Func( + [IdentityNumber, AuthnMethodData], + [IDL.Variant({ 'Ok' : IDL.Null, 'Err' : AuthnMethodAddError })], + [], + ), + 'authn_method_confirm' : IDL.Func( + [IdentityNumber, IDL.Text], + [ + IDL.Variant({ + 'Ok' : IDL.Null, + 'Err' : AuthnMethodConfirmationError, + }), + ], + [], + ), + 'authn_method_metadata_replace' : IDL.Func( + [IdentityNumber, PublicKey, MetadataMapV2], + [ + IDL.Variant({ + 'Ok' : IDL.Null, + 'Err' : AuthnMethodMetadataReplaceError, + }), + ], + [], + ), + 'authn_method_register' : IDL.Func( + [IdentityNumber, AuthnMethodData], + [ + IDL.Variant({ + 'Ok' : AuthnMethodConfirmationCode, + 'Err' : AuthnMethodRegisterError, + }), + ], + [], + ), + 'authn_method_registration_mode_enter' : IDL.Func( + [IdentityNumber, IDL.Opt(RegistrationId)], + [ + IDL.Variant({ + 'Ok' : IDL.Record({ 'expiration' : Timestamp }), + 'Err' : AuthnMethodRegistrationModeEnterError, + }), + ], + [], + ), + 'authn_method_registration_mode_exit' : IDL.Func( + [IdentityNumber, IDL.Opt(AuthnMethodData)], + [ + IDL.Variant({ + 'Ok' : IDL.Null, + 'Err' : AuthnMethodRegistrationModeExitError, + }), + ], + [], + ), + 'authn_method_remove' : IDL.Func( + [IdentityNumber, PublicKey], + [IDL.Variant({ 'Ok' : IDL.Null, 'Err' : IDL.Null })], + [], + ), + 'authn_method_replace' : IDL.Func( + [IdentityNumber, PublicKey, AuthnMethodData], + [IDL.Variant({ 'Ok' : IDL.Null, 'Err' : AuthnMethodReplaceError })], + [], + ), + 'authn_method_security_settings_replace' : IDL.Func( + [IdentityNumber, PublicKey, AuthnMethodSecuritySettings], + [ + IDL.Variant({ + 'Ok' : IDL.Null, + 'Err' : AuthnMethodSecuritySettingsReplaceError, + }), + ], + [], + ), + 'authn_method_session_info' : IDL.Func( + [IdentityNumber], + [IDL.Opt(AuthnMethodSessionInfo)], + ['query'], + ), + 'authn_method_session_register' : IDL.Func( + [IdentityNumber], + [ + IDL.Variant({ + 'Ok' : AuthnMethodConfirmationCode, + 'Err' : AuthnMethodRegisterError, + }), + ], + [], + ), + 'check_captcha' : IDL.Func( + [CheckCaptchaArg], + [ + IDL.Variant({ + 'Ok' : IdRegNextStepResult, + 'Err' : CheckCaptchaError, + }), + ], + [], + ), + 'config' : IDL.Func([], [InternetIdentityInit], ['query']), + 'create_account' : IDL.Func( + [UserNumber, FrontendHostname, IDL.Text], + [IDL.Variant({ 'Ok' : AccountInfo, 'Err' : CreateAccountError })], + [], + ), + 'create_challenge' : IDL.Func([], [Challenge], []), + 'deploy_archive' : IDL.Func([IDL.Vec(IDL.Nat8)], [DeployArchiveResult], []), + 'enter_device_registration_mode' : IDL.Func([UserNumber], [Timestamp], []), + 'exit_device_registration_mode' : IDL.Func([UserNumber], [], []), + 'fetch_entries' : IDL.Func([], [IDL.Vec(BufferedArchiveEntry)], []), + 'get_account_delegation' : IDL.Func( + [ + UserNumber, + FrontendHostname, + IDL.Opt(AccountNumber), + SessionKey, + Timestamp, + ], + [ + IDL.Variant({ + 'Ok' : SignedDelegation, + 'Err' : AccountDelegationError, + }), + ], + ['query'], + ), + 'get_accounts' : IDL.Func( + [UserNumber, FrontendHostname], + [ + IDL.Variant({ + 'Ok' : IDL.Vec(AccountInfo), + 'Err' : GetAccountsError, + }), + ], + ['query'], + ), + 'get_anchor_credentials' : IDL.Func( + [UserNumber], + [AnchorCredentials], + ['query'], + ), + 'get_anchor_info' : IDL.Func([UserNumber], [IdentityAnchorInfo], []), + 'get_attributes' : IDL.Func( + [GetAttributesRequest], + [ + IDL.Variant({ + 'Ok' : CertifiedAttributes, + 'Err' : GetAttributesError, + }), + ], + ['query'], + ), + 'get_default_account' : IDL.Func( + [UserNumber, FrontendHostname], + [IDL.Variant({ 'Ok' : AccountInfo, 'Err' : GetDefaultAccountError })], + ['query'], + ), + 'get_delegation' : IDL.Func( + [UserNumber, FrontendHostname, SessionKey, Timestamp], + [GetDelegationResponse], + ['query'], + ), + 'get_id_alias' : IDL.Func( + [GetIdAliasRequest], + [IDL.Variant({ 'Ok' : IdAliasCredentials, 'Err' : GetIdAliasError })], + ['query'], + ), + 'get_principal' : IDL.Func( + [UserNumber, FrontendHostname], + [IDL.Principal], + ['query'], + ), + 'http_request' : IDL.Func([HttpRequest], [HttpResponse], ['query']), + 'identity_authn_info' : IDL.Func( + [IdentityNumber], + [IDL.Variant({ 'Ok' : IdentityAuthnInfo, 'Err' : IDL.Null })], + ['query'], + ), + 'identity_info' : IDL.Func( + [IdentityNumber], + [IDL.Variant({ 'Ok' : IdentityInfo, 'Err' : IdentityInfoError })], + [], + ), + 'identity_metadata_replace' : IDL.Func( + [IdentityNumber, MetadataMapV2], + [ + IDL.Variant({ + 'Ok' : IDL.Null, + 'Err' : IdentityMetadataReplaceError, + }), + ], + [], + ), + 'identity_properties_replace' : IDL.Func( + [IdentityNumber, IdentityPropertiesReplace], + [ + IDL.Variant({ + 'Ok' : IDL.Null, + 'Err' : IdentityPropertiesReplaceError, + }), + ], + [], + ), + 'identity_registration_finish' : IDL.Func( + [IdRegFinishArg], + [IDL.Variant({ 'Ok' : IdRegFinishResult, 'Err' : IdRegFinishError })], + [], + ), + 'identity_registration_start' : IDL.Func( + [], + [IDL.Variant({ 'Ok' : IdRegNextStepResult, 'Err' : IdRegStartError })], + [], + ), + 'init_salt' : IDL.Func([], [], []), + 'lookup' : IDL.Func([UserNumber], [IDL.Vec(DeviceData)], ['query']), + 'lookup_by_registration_mode_id' : IDL.Func( + [RegistrationId], + [IDL.Opt(IdentityNumber)], + ['query'], + ), + 'lookup_caller_identity_by_recovery_phrase' : IDL.Func( + [], + [IDL.Opt(IdentityNumber)], + [], + ), + 'lookup_device_key' : IDL.Func( + [IDL.Vec(IDL.Nat8)], + [IDL.Opt(DeviceKeyWithAnchor)], + ['query'], + ), + 'openid_credential_add' : IDL.Func( + [IdentityNumber, JWT, Salt], + [IDL.Variant({ 'Ok' : IDL.Null, 'Err' : OpenIdCredentialAddError })], + [], + ), + 'openid_credential_remove' : IDL.Func( + [IdentityNumber, OpenIdCredentialKey], + [IDL.Variant({ 'Ok' : IDL.Null, 'Err' : OpenIdCredentialRemoveError })], + [], + ), + 'openid_get_delegation' : IDL.Func( + [JWT, Salt, SessionKey, Timestamp], + [ + IDL.Variant({ + 'Ok' : SignedDelegation, + 'Err' : OpenIdDelegationError, + }), + ], + ['query'], + ), + 'openid_identity_registration_finish' : IDL.Func( + [OpenIDRegFinishArg], + [IDL.Variant({ 'Ok' : IdRegFinishResult, 'Err' : IdRegFinishError })], + [], + ), + 'openid_prepare_delegation' : IDL.Func( + [JWT, Salt, SessionKey], + [ + IDL.Variant({ + 'Ok' : OpenIdPrepareDelegationResponse, + 'Err' : OpenIdDelegationError, + }), + ], + [], + ), + 'prepare_account_delegation' : IDL.Func( + [ + UserNumber, + FrontendHostname, + IDL.Opt(AccountNumber), + SessionKey, + IDL.Opt(IDL.Nat64), + ], + [ + IDL.Variant({ + 'Ok' : PrepareAccountDelegation, + 'Err' : AccountDelegationError, + }), + ], + [], + ), + 'prepare_attributes' : IDL.Func( + [PrepareAttributeRequest], + [ + IDL.Variant({ + 'Ok' : PrepareAttributeResponse, + 'Err' : PrepareAttributeError, + }), + ], + [], + ), + 'prepare_delegation' : IDL.Func( + [UserNumber, FrontendHostname, SessionKey, IDL.Opt(IDL.Nat64)], + [UserKey, Timestamp], + [], + ), + 'prepare_id_alias' : IDL.Func( + [PrepareIdAliasRequest], + [IDL.Variant({ 'Ok' : PreparedIdAlias, 'Err' : PrepareIdAliasError })], + [], + ), + 'register' : IDL.Func( + [DeviceData, ChallengeResult, IDL.Opt(IDL.Principal)], + [RegisterResponse], + [], + ), + 'remove' : IDL.Func([UserNumber, DeviceKey], [], []), + 'replace' : IDL.Func([UserNumber, DeviceKey, DeviceData], [], []), + 'set_default_account' : IDL.Func( + [UserNumber, FrontendHostname, IDL.Opt(AccountNumber)], + [IDL.Variant({ 'Ok' : AccountInfo, 'Err' : SetDefaultAccountError })], + [], + ), + 'stats' : IDL.Func([], [InternetIdentityStats], ['query']), + 'update' : IDL.Func([UserNumber, DeviceKey, DeviceData], [], []), + 'update_account' : IDL.Func( + [UserNumber, FrontendHostname, IDL.Opt(AccountNumber), AccountUpdate], + [IDL.Variant({ 'Ok' : AccountInfo, 'Err' : UpdateAccountError })], + [], + ), + 'verify_tentative_device' : IDL.Func( + [UserNumber, IDL.Text], + [VerifyTentativeDeviceResponse], + [], + ), + }); +}; + +export const init = ({ IDL }) => { + const ArchiveConfig = IDL.Record({ + 'polling_interval_ns' : IDL.Nat64, + 'entries_buffer_limit' : IDL.Nat64, + 'module_hash' : IDL.Vec(IDL.Nat8), + 'entries_fetch_limit' : IDL.Nat16, + }); + const AnalyticsConfig = IDL.Variant({ + 'Plausible' : IDL.Record({ + 'domain' : IDL.Opt(IDL.Text), + 'track_localhost' : IDL.Opt(IDL.Bool), + 'hash_mode' : IDL.Opt(IDL.Bool), + 'api_host' : IDL.Opt(IDL.Text), + }), + }); + const OpenIdEmailVerification = IDL.Variant({ + 'Google' : IDL.Null, + 'Unknown' : IDL.Null, + 'Microsoft' : IDL.Null, + }); + const OpenIdConfig = IDL.Record({ + 'auth_uri' : IDL.Text, + 'jwks_uri' : IDL.Text, + 'logo' : IDL.Text, + 'name' : IDL.Text, + 'fedcm_uri' : IDL.Opt(IDL.Text), + 'email_verification' : IDL.Opt(OpenIdEmailVerification), + 'issuer' : IDL.Text, + 'auth_scope' : IDL.Vec(IDL.Text), + 'client_id' : IDL.Text, + }); + const CaptchaConfig = IDL.Record({ + 'max_unsolved_captchas' : IDL.Nat64, + 'captcha_trigger' : IDL.Variant({ + 'Dynamic' : IDL.Record({ + 'reference_rate_sampling_interval_s' : IDL.Nat64, + 'threshold_pct' : IDL.Nat16, + 'current_rate_sampling_interval_s' : IDL.Nat64, + }), + 'Static' : IDL.Variant({ + 'CaptchaDisabled' : IDL.Null, + 'CaptchaEnabled' : IDL.Null, + }), + }), + }); + const DummyAuthConfig = IDL.Record({ 'prompt_for_index' : IDL.Bool }); + const RateLimitConfig = IDL.Record({ + 'max_tokens' : IDL.Nat64, + 'time_per_token_ns' : IDL.Nat64, + }); + const InternetIdentityInit = IDL.Record({ + 'fetch_root_key' : IDL.Opt(IDL.Bool), + 'is_production' : IDL.Opt(IDL.Bool), + 'backend_canister_id' : IDL.Opt(IDL.Principal), + 'enable_dapps_explorer' : IDL.Opt(IDL.Bool), + 'assigned_user_number_range' : IDL.Opt(IDL.Tuple(IDL.Nat64, IDL.Nat64)), + 'new_flow_origins' : IDL.Opt(IDL.Vec(IDL.Text)), + 'archive_config' : IDL.Opt(ArchiveConfig), + 'canister_creation_cycles_cost' : IDL.Opt(IDL.Nat64), + 'analytics_config' : IDL.Opt(IDL.Opt(AnalyticsConfig)), + 'related_origins' : IDL.Opt(IDL.Vec(IDL.Text)), + 'openid_configs' : IDL.Opt(IDL.Vec(OpenIdConfig)), + 'backend_origin' : IDL.Opt(IDL.Text), + 'captcha_config' : IDL.Opt(CaptchaConfig), + 'dummy_auth' : IDL.Opt(IDL.Opt(DummyAuthConfig)), + 'register_rate_limit' : IDL.Opt(RateLimitConfig), + }); + + return [IDL.Opt(InternetIdentityInit)]; +}; \ No newline at end of file diff --git a/examples/t_ecdsa/frontend/src/bindings/declarations/t_ecdsa_backend.did.d.ts b/examples/t_ecdsa/frontend/src/bindings/declarations/t_ecdsa_backend.did.d.ts new file mode 100644 index 0000000..06ddd12 --- /dev/null +++ b/examples/t_ecdsa/frontend/src/bindings/declarations/t_ecdsa_backend.did.d.ts @@ -0,0 +1,30 @@ +/* eslint-disable */ + +// @ts-nocheck + +// This file was automatically generated by @icp-sdk/bindgen@0.2.2. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import type { ActorMethod } from '@icp-sdk/core/agent'; +import type { IDL } from '@icp-sdk/core/candid'; +import type { Principal } from '@icp-sdk/core/principal'; + +export interface _SERVICE { + 'getEvmAddress' : ActorMethod<[], string>, + 'getNewPublicKey' : ActorMethod<[], string>, + 'getPublicKey' : ActorMethod<[], string>, + 'signWithEcdsa' : ActorMethod<[string], string>, + 'signWithEthereum' : ActorMethod<[string], string>, + 'signWithEvmWallet' : ActorMethod<[Uint8Array], string>, + 'verifyWithEcdsa' : ActorMethod< + [{ 'signature' : string, 'publicKey' : string, 'message' : string }], + boolean + >, + 'verifyWithEthereum' : ActorMethod< + [{ 'signature' : string, 'message' : string, 'ethereumAddress' : string }], + boolean + >, +} +export declare const idlFactory: IDL.InterfaceFactory; +export declare const init: (args: { IDL: typeof IDL }) => IDL.Type[]; \ No newline at end of file diff --git a/examples/t_ecdsa/frontend/src/bindings/declarations/t_ecdsa_backend.did.js b/examples/t_ecdsa/frontend/src/bindings/declarations/t_ecdsa_backend.did.js new file mode 100644 index 0000000..83949d5 --- /dev/null +++ b/examples/t_ecdsa/frontend/src/bindings/declarations/t_ecdsa_backend.did.js @@ -0,0 +1,44 @@ +/* eslint-disable */ + +// @ts-nocheck + +// This file was automatically generated by @icp-sdk/bindgen@0.2.2. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { IDL } from '@icp-sdk/core/candid'; + +export const idlFactory = ({ IDL }) => { + return IDL.Service({ + 'getEvmAddress' : IDL.Func([], [IDL.Text], ['query']), + 'getNewPublicKey' : IDL.Func([], [IDL.Text], []), + 'getPublicKey' : IDL.Func([], [IDL.Text], ['query']), + 'signWithEcdsa' : IDL.Func([IDL.Text], [IDL.Text], []), + 'signWithEthereum' : IDL.Func([IDL.Text], [IDL.Text], []), + 'signWithEvmWallet' : IDL.Func([IDL.Vec(IDL.Nat8)], [IDL.Text], []), + 'verifyWithEcdsa' : IDL.Func( + [ + IDL.Record({ + 'signature' : IDL.Text, + 'publicKey' : IDL.Text, + 'message' : IDL.Text, + }), + ], + [IDL.Bool], + [], + ), + 'verifyWithEthereum' : IDL.Func( + [ + IDL.Record({ + 'signature' : IDL.Text, + 'message' : IDL.Text, + 'ethereumAddress' : IDL.Text, + }), + ], + [IDL.Bool], + [], + ), + }); +}; + +export const init = ({ IDL }) => { return []; }; \ No newline at end of file diff --git a/examples/t_ecdsa/frontend/src/bindings/internet_identity.ts b/examples/t_ecdsa/frontend/src/bindings/internet_identity.ts new file mode 100644 index 0000000..4211597 --- /dev/null +++ b/examples/t_ecdsa/frontend/src/bindings/internet_identity.ts @@ -0,0 +1,4922 @@ +/* eslint-disable */ + +// @ts-nocheck + +// This file was automatically generated by @icp-sdk/bindgen@0.2.2. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Actor, HttpAgent, type HttpAgentOptions, type ActorConfig, type Agent, type ActorSubclass } from "@icp-sdk/core/agent"; +import type { Principal } from "@icp-sdk/core/principal"; +import { idlFactory, type _SERVICE } from "./declarations/internet_identity.did"; +export interface Some { + __kind__: "Some"; + value: T; +} +export interface None { + __kind__: "None"; +} +export type Option = Some | None; +function some(value: T): Some { + return { + __kind__: "Some", + value: value + }; +} +function none(): None { + return { + __kind__: "None" + }; +} +function isNone(option: Option): option is None { + return option.__kind__ === "None"; +} +function isSome(option: Option): option is Some { + return option.__kind__ === "Some"; +} +function unwrap(option: Option): T { + if (isNone(option)) { + throw new Error("unwrap: none"); + } + return option.value; +} +function candid_some(value: T): [T] { + return [ + value + ]; +} +function candid_none(): [] { + return []; +} +function record_opt_to_undefined(arg: T | null): T | undefined { + return arg == null ? undefined : arg; +} +export interface AnchorCredentials { + recovery_phrases: Array; + credentials: Array; + recovery_credentials: Array; +} +export interface CertifiedAttribute { + key: string; + signature: Uint8Array; + value: Uint8Array; +} +export interface CaptchaConfig { + /** + * Maximum number of unsolved captchas. + */ + max_unsolved_captchas: bigint; + /** + * Configuration for when captcha protection should kick in. + */ + captcha_trigger: { + __kind__: "Dynamic"; + /** + * Based on the rate of registrations compared to some reference time frame and allowing some leeway. + */ + Dynamic: { + /** + * Length of the interval in seconds used to sample the reference rate of registrations. + */ + reference_rate_sampling_interval_s: bigint; + /** + * Percentage of increased registration rate observed in the current rate sampling interval (compared to + * reference rate) at which II will enable captcha for new registrations. + */ + threshold_pct: number; + /** + * Length of the interval in seconds used to sample the current rate of registrations. + */ + current_rate_sampling_interval_s: bigint; + }; + } | { + __kind__: "Static"; + /** + * Statically enable / disable captcha + */ + Static: Variant_CaptchaDisabled_CaptchaEnabled; + }; +} +export interface IdAliasCredentials { + rp_id_alias_credential: SignedIdAlias; + issuer_id_alias_credential: SignedIdAlias; +} +export interface GetAttributesRequest { + /** + * Origin of the relying party in the attribute sharing flow. + */ + origin: FrontendHostname; + /** + * II account for the relying party. + */ + account_number?: AccountNumber; + /** + * The attribute to be retrieved, must be a subset of certified_attributes from + * the prepare_attributes response. + */ + attributes: Array<[string, Uint8Array]>; + /** + * Timestamp received from the prepare_attributes call. + */ + issued_at_timestamp_ns: Timestamp; + /** + * Identity for which the attributes should be prepared. + */ + identity_number: IdentityNumber; +} +export type RegistrationFlowNextStep = { + __kind__: "CheckCaptcha"; + /** + * Supply the captcha solution using check_captcha + */ + CheckCaptcha: { + captcha_png_base64: string; + }; +} | { + __kind__: "Finish"; + /** + * Finish the registration using identity_registration_finish + */ + Finish: null; +}; +export type CreateAccountError = { + __kind__: "AccountLimitReached"; + AccountLimitReached: null; +} | { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +} | { + __kind__: "NameTooLong"; + NameTooLong: null; +}; +export interface CheckCaptchaArg { + solution: string; +} +export type UpdateAccountError = { + __kind__: "AccountLimitReached"; + AccountLimitReached: null; +} | { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +} | { + __kind__: "NameTooLong"; + NameTooLong: null; +}; +export interface IdRegFinishArg { + name?: string; + authn_method: AuthnMethodData; +} +export interface DeviceKeyWithAnchor { + pubkey: DeviceKey; + anchor_number: UserNumber; +} +export interface Token { +} +export type OpenIdCredentialKey = [Iss, Sub]; +export type AccountDelegationError = { + __kind__: "NoSuchDelegation"; + NoSuchDelegation: null; +} | { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +}; +export interface IdRegNextStepResult { + /** + * The next step in the registration flow + */ + next_step: RegistrationFlowNextStep; +} +export type UserNumber = bigint; +export interface CertifiedAttributes { + expires_at_timestamp_ns: Timestamp; + certified_attributes: Array; +} +export interface SignedDelegation { + signature: Uint8Array; + delegation: Delegation; +} +export interface StreamingCallbackHttpResponse { + token?: Token; + body: Uint8Array; +} +export type CredentialId = Uint8Array; +export interface AuthnMethodRegistrationInfo { + /** + * The timestamp at which the identity will turn off registration mode + * (and the authentication method will be forgotten, if any, and if not verified) + */ + expiration: Timestamp; + /** + * If present, the user has registered a new session. This new session needs to be confirmed before + * 'expiration' in order for it be authorized to register an authentication method to the identity. + */ + session?: Principal; + /** + * If present, the user has registered a new authentication method. This new authentication + * method needs to be confirmed before 'expiration' in order to be added to the identity. + */ + authn_method?: AuthnMethodData; +} +export type IdRegFinishError = { + __kind__: "NoRegistrationFlow"; + /** + * No registration flow ongoing for the caller. + */ + NoRegistrationFlow: null; +} | { + __kind__: "UnexpectedCall"; + /** + * This call is unexpected, see next_step. + */ + UnexpectedCall: { + next_step: RegistrationFlowNextStep; + }; +} | { + __kind__: "InvalidAuthnMethod"; + /** + * The supplied authn_method is not valid. + */ + InvalidAuthnMethod: string; +} | { + __kind__: "StorageError"; + /** + * Error while persisting the new identity. + */ + StorageError: string; +}; +export interface DummyAuthConfig { + /** + * Prompts user for a index value (0 - 255) when set to true, + * this is used in e2e to have multiple dummy auth identities. + */ + prompt_for_index: boolean; +} +export type GetAccountsError = { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +}; +export interface IdentityAnchorInfo { + /** + * The name of the Internet Identity + */ + name?: string; + /** + * The timestamp at which the anchor was created + */ + created_at?: Timestamp; + /** + * All devices that can authenticate to this anchor + */ + devices: Array; + /** + * OpenID accounts linked to this anchor + */ + openid_credentials?: Array; + /** + * Device registration status used when adding devices, see DeviceRegistrationInfo + */ + device_registration?: DeviceRegistrationInfo; +} +export type AccountNumber = bigint; +export type AuthnMethodMetadataReplaceError = { + __kind__: "AuthnMethodNotFound"; + /** + * No authentication method found with the given public key. + */ + AuthnMethodNotFound: null; +} | { + __kind__: "InvalidMetadata"; + InvalidMetadata: string; +}; +export interface AccountUpdate { + name?: string; +} +export interface PrepareAttributeRequest { + /** + * Origin of the relying party in the attribute sharing flow. + */ + origin: FrontendHostname; + /** + * The attribute to be prepared. + */ + attribute_keys: Array; + /** + * II account for the relying party. + */ + account_number?: AccountNumber; + /** + * Identity for which the attributes should be prepared. + */ + identity_number: IdentityNumber; +} +export interface AuthnMethodSecuritySettings { + protection: AuthnMethodProtection; + purpose: AuthnMethodPurpose; +} +export interface IdentityInfo { + authn_methods: Array; + /** + * Authentication method independent metadata + */ + metadata: MetadataMapV2; + name?: string; + /** + * The timestamp at which the anchor was created + */ + created_at?: Timestamp; + authn_method_registration?: AuthnMethodRegistrationInfo; + openid_credentials?: Array; +} +export interface WebAuthn { + pubkey: PublicKey; + /** + * Authenticator Attestation Global Unique Identifier (AAGUID) + */ + aaguid?: Aaguid; + credential_id: CredentialId; +} +export interface ArchiveConfig { + /** + * Polling interval to fetch new entries from II (in nanoseconds). + * Changes to this parameter will only take effect after an archive deployment. + */ + polling_interval_ns: bigint; + /** + * Buffered archive entries limit. If reached, II will stop accepting new anchor operations + * until the buffered operations are acknowledged by the archive. + */ + entries_buffer_limit: bigint; + /** + * The allowed module hash of the archive canister. + * Changing this parameter does _not_ deploy the archive, but enable archive deployments with the + * corresponding wasm module. + */ + module_hash: Uint8Array; + /** + * The maximum number of entries to be transferred to the archive per call. + */ + entries_fetch_limit: number; +} +export type SetDefaultAccountError = { + __kind__: "NoSuchOrigin"; + NoSuchOrigin: { + anchor_number: UserNumber; + }; +} | { + __kind__: "NoSuchAnchor"; + NoSuchAnchor: null; +} | { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +} | { + __kind__: "NoSuchAccount"; + NoSuchAccount: { + origin: FrontendHostname; + anchor_number: UserNumber; + }; +}; +export type IdentityNumber = bigint; +export type AuthnMethod = { + __kind__: "PubKey"; + PubKey: PublicKeyAuthn; +} | { + __kind__: "WebAuthn"; + WebAuthn: WebAuthn; +}; +export interface PreparedIdAlias { + rp_id_alias_jwt: string; + issuer_id_alias_jwt: string; + canister_sig_pk_der: PublicKey; +} +export type AddTentativeDeviceResponse = { + __kind__: "device_registration_mode_off"; + /** + * Device registration mode is off, either due to timeout or because it was never enabled. + */ + device_registration_mode_off: null; +} | { + __kind__: "another_device_tentatively_added"; + /** + * There is another device already added tentatively + */ + another_device_tentatively_added: null; +} | { + __kind__: "passkey_with_this_public_key_is_already_used"; + /** + * Passkey with this public key is already used + */ + passkey_with_this_public_key_is_already_used: null; +} | { + __kind__: "added_tentatively"; + /** + * The device was tentatively added. + */ + added_tentatively: { + verification_code: string; + /** + * Expiration date, in nanos since the epoch + */ + device_registration_timeout: Timestamp; + }; +}; +export type GetAccountError = { + __kind__: "NoSuchOrigin"; + NoSuchOrigin: { + anchor_number: UserNumber; + }; +} | { + __kind__: "NoSuchAccount"; + NoSuchAccount: { + origin: FrontendHostname; + anchor_number: UserNumber; + }; +}; +export interface AuthnMethodConfirmationCode { + confirmation_code: string; + expiration: Timestamp; +} +export interface IdentityPropertiesReplace { + name?: string; +} +export type CheckCaptchaError = { + __kind__: "NoRegistrationFlow"; + /** + * No registration flow ongoing for the caller. + */ + NoRegistrationFlow: null; +} | { + __kind__: "UnexpectedCall"; + /** + * This call is unexpected, see next_step. + */ + UnexpectedCall: { + next_step: RegistrationFlowNextStep; + }; +} | { + __kind__: "WrongSolution"; + /** + * The supplied solution was wrong. Try again with the new captcha. + */ + WrongSolution: { + new_captcha_png_base64: string; + }; +}; +export interface DeviceWithUsage { + alias: string; + last_usage?: Timestamp; + metadata?: MetadataMap; + origin?: string; + protection: DeviceProtection; + pubkey: DeviceKey; + key_type: KeyType; + aaguid?: Aaguid; + purpose: Purpose; + credential_id?: CredentialId; +} +export interface DeviceData { + alias: string; + /** + * Metadata map for additional device information. + * + * Note: some fields above will be moved to the metadata map in the future. + * All field names of `DeviceData` (such as 'alias', 'origin, etc.) are + * reserved and cannot be written. + * In addition, the keys "usage" and "authenticator_attachment" are reserved as well. + */ + metadata?: MetadataMap; + origin?: string; + protection: DeviceProtection; + pubkey: DeviceKey; + key_type: KeyType; + aaguid?: Aaguid; + purpose: Purpose; + credential_id?: CredentialId; +} +export interface WebAuthnCredential { + pubkey: PublicKey; + credential_id: CredentialId; +} +export type DeviceKey = PublicKey; +export interface Delegation { + pubkey: PublicKey; + targets?: Array; + expiration: Timestamp; +} +export type OpenIdCredentialAddError = { + __kind__: "OpenIdCredentialAlreadyRegistered"; + OpenIdCredentialAlreadyRegistered: null; +} | { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "JwtExpired"; + JwtExpired: null; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +} | { + __kind__: "JwtVerificationFailed"; + JwtVerificationFailed: null; +}; +export type Aaguid = Uint8Array; +export interface HttpResponse { + body: Uint8Array; + headers: Array; + upgrade?: boolean; + status_code: number; +} +export interface PrepareAttributeResponse { + attributes: Array<[string, Uint8Array]>; + issued_at_timestamp_ns: Timestamp; +} +export interface SignedIdAlias { + credential_jws: string; + id_alias: Principal; + id_dapp: Principal; +} +export interface OpenIdPrepareDelegationResponse { + user_key: UserKey; + expiration: Timestamp; + anchor_number: UserNumber; +} +export interface ArchiveInfo { + /** + * Configuration parameters related to the II archive. + */ + archive_config?: ArchiveConfig; + /** + * Canister id of the archive or empty if no archive has been deployed yet. + */ + archive_canister?: Principal; +} +export type Salt = Uint8Array; +export interface InternetIdentityInit { + /** + * Configuration to fetch root key or not from frontend assets + */ + fetch_root_key?: boolean; + /** + * Configuration to set the canister as production mode. + * For now, this is used only to show or hide the banner. + */ + is_production?: boolean; + /** + * Backend canister ID, needed for backward compatibility. + */ + backend_canister_id?: Principal; + /** + * Configuration to show dapps explorer or not + */ + enable_dapps_explorer?: boolean; + /** + * Set lowest and highest anchor + */ + assigned_user_number_range?: [bigint, bigint]; + /** + * Configuration for New Origin Flows. + * If present, list of origins using the new authentication flow. + */ + new_flow_origins?: Array; + /** + * Configuration parameters related to the II archive. + * Note: some parameters changes (like the polling interval) will only take effect after an archive deployment. + * See ArchiveConfig for details. + */ + archive_config?: ArchiveConfig; + /** + * Set the amounts of cycles sent with the create canister message. + * This is configurable because in the staging environment cycles are required. + * The canister creation cost on mainnet is currently 100'000'000'000 cycles. If this value is higher thant the + * canister creation cost, the newly created canister will keep extra cycles. + */ + canister_creation_cycles_cost?: bigint; + /** + * Configuration for Web Analytics + */ + analytics_config?: AnalyticsConfig | null; + /** + * Configuration for Related Origins Requests. + * If present, list of origins from where registration is allowed. + */ + related_origins?: Array; + /** + * Configurations for OpenID clients + */ + openid_configs?: Array; + /** + * Backend origin, needed to sync configuration with frontend. + */ + backend_origin?: string; + /** + * Configuration of the captcha in the registration flow. + */ + captcha_config?: CaptchaConfig; + /** + * Configuration for dummy authentication used in e2e tests. + */ + dummy_auth?: DummyAuthConfig | null; + /** + * Rate limit for the `register` call. + */ + register_rate_limit?: RateLimitConfig; +} +/** + * Map with some variants for the value type. + * Note, due to the Candid mapping this must be a tuple type thus we cannot name the fields `key` and `value`. + */ +export type MetadataMapV2 = Array<[string, { + __kind__: "Map"; + Map: MetadataMapV2; + } | { + __kind__: "String"; + String: string; + } | { + __kind__: "Bytes"; + Bytes: Uint8Array; + }]>; +export interface BufferedArchiveEntry { + sequence_number: bigint; + entry: Uint8Array; + anchor_number: UserNumber; + timestamp: Timestamp; +} +export type GetDelegationResponse = { + __kind__: "no_such_delegation"; + /** + * The signature is not ready. Maybe retry by calling `prepare_delegation` + */ + no_such_delegation: null; +} | { + __kind__: "signed_delegation"; + /** + * The signed delegation was successfully retrieved. + */ + signed_delegation: SignedDelegation; +}; +export type AuthnMethodReplaceError = { + __kind__: "PasskeyWithThisPublicKeyIsAlreadyUsed"; + /** + * Passkey with this public key is already used + */ + PasskeyWithThisPublicKeyIsAlreadyUsed: null; +} | { + __kind__: "AuthnMethodNotFound"; + /** + * No authentication method found with the given public key. + */ + AuthnMethodNotFound: null; +} | { + __kind__: "InvalidMetadata"; + InvalidMetadata: string; +}; +export interface IdRegFinishResult { + identity_number: bigint; +} +/** + * Map with some variants for the value type. + * Note, due to the Candid mapping this must be a tuple type thus we cannot name the fields `key` and `value`. + */ +export type MetadataMap = Array<[string, { + __kind__: "map"; + map: MetadataMap; + } | { + __kind__: "string"; + string: string; + } | { + __kind__: "bytes"; + bytes: Uint8Array; + }]>; +export type GetDefaultAccountError = { + __kind__: "NoSuchOrigin"; + NoSuchOrigin: { + anchor_number: UserNumber; + }; +} | { + __kind__: "NoSuchAnchor"; + NoSuchAnchor: null; +} | { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +}; +export type PrepareIdAliasError = { + __kind__: "InternalCanisterError"; + /** + * Internal canister error. See the error message for details. + */ + InternalCanisterError: string; +} | { + __kind__: "Unauthorized"; + /** + * The principal is not authorized to call this method with the given arguments. + */ + Unauthorized: Principal; +}; +export type Iss = string; +export interface DeviceRegistrationInfo { + /** + * If present, the user has registered a new authentication method. This new authentication + * method needs to be confirmed before 'expiration' in order to be added to the identity. + */ + tentative_device?: DeviceData; + /** + * The timestamp at which the anchor will turn off registration mode + * (and the tentative device will be forgotten, if any, and if not verified) + */ + expiration: Timestamp; + /** + * If present, the user has registered a new session. This new session needs to be confirmed before + * 'expiration' in order for it be authorized to register an authentication method to the identity. + */ + tentative_session?: Principal; +} +export type AuthnMethodConfirmationError = { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "RegistrationModeOff"; + /** + * Authentication method registration mode is off, either due to timeout or because it was never enabled. + */ + RegistrationModeOff: null; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +} | { + __kind__: "NoAuthnMethodToConfirm"; + /** + * There is no registered authentication method to be confirmed. + */ + NoAuthnMethodToConfirm: null; +} | { + __kind__: "WrongCode"; + /** + * Wrong confirmation code entered. Retry with correct code. + */ + WrongCode: { + retries_left: number; + }; +}; +export interface InternetIdentityStats { + storage_layout_version: number; + users_registered: bigint; + assigned_user_number_range: [bigint, bigint]; + archive_info: ArchiveInfo; + canister_creation_cycles_cost: bigint; + /** + * Map from event aggregation to a sorted list of top 100 sub-keys to their weights. + * Example: {"prepare_delegation_count 24h ic0.app": [{"https://dapp.com", 100}, {"https://dapp2.com", 50}]} + */ + event_aggregations: Array<[string, Array<[string, bigint]>]>; +} +export type IdentityPropertiesReplaceError = { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +} | { + __kind__: "NameTooLong"; + NameTooLong: { + limit: bigint; + }; +} | { + __kind__: "StorageSpaceExceeded"; + StorageSpaceExceeded: { + space_required: bigint; + space_available: bigint; + }; +}; +export interface PublicKeyAuthn { + pubkey: PublicKey; +} +export type CaptchaResult = ChallengeResult; +export type AnalyticsConfig = { + __kind__: "Plausible"; + Plausible: { + domain?: string; + track_localhost?: boolean; + hash_mode?: boolean; + api_host?: string; + }; +}; +export type AuthnMethodRegisterError = { + __kind__: "PasskeyWithThisPublicKeyIsAlreadyUsed"; + /** + * Passkey with this public key is already used + */ + PasskeyWithThisPublicKeyIsAlreadyUsed: null; +} | { + __kind__: "RegistrationModeOff"; + /** + * Authentication method registration mode is off, either due to timeout or because it was never enabled. + */ + RegistrationModeOff: null; +} | { + __kind__: "RegistrationAlreadyInProgress"; + /** + * There is another authentication method already registered that needs to be confirmed first. + */ + RegistrationAlreadyInProgress: null; +} | { + __kind__: "NotSelfAuthenticating"; + /** + * The caller's principal is not self-authenticating. + */ + NotSelfAuthenticating: Principal; +} | { + __kind__: "InvalidMetadata"; + /** + * The metadata of the provided authentication method contains invalid entries. + */ + InvalidMetadata: string; +}; +export type RegistrationId = string; +export type LookupByRegistrationIdError = { + __kind__: "InvalidRegistrationId"; + InvalidRegistrationId: string; +}; +export type FrontendHostname = string; +export type VerifyTentativeDeviceResponse = { + __kind__: "device_registration_mode_off"; + /** + * Device registration mode is off, either due to timeout or because it was never enabled. + */ + device_registration_mode_off: null; +} | { + __kind__: "verified"; + /** + * The device was successfully verified. + */ + verified: null; +} | { + __kind__: "wrong_code"; + /** + * Wrong verification code entered. Retry with correct code. + */ + wrong_code: { + retries_left: number; + }; +} | { + __kind__: "no_device_to_verify"; + /** + * There is no tentative device to be verified. + */ + no_device_to_verify: null; +}; +export interface AuthnMethodSessionInfo { + name?: string; + created_at?: Timestamp; +} +export type StreamingStrategy = { + __kind__: "Callback"; + Callback: { + token: Token; + callback: [Principal, string]; + }; +}; +export interface PrepareIdAliasRequest { + /** + * Origin of the issuer in the attribute sharing flow. + */ + issuer: FrontendHostname; + /** + * Origin of the relying party in the attribute sharing flow. + */ + relying_party: FrontendHostname; + /** + * Identity for which the IdAlias should be generated. + */ + identity_number: IdentityNumber; +} +export interface GetIdAliasRequest { + rp_id_alias_jwt: string; + issuer: FrontendHostname; + issuer_id_alias_jwt: string; + relying_party: FrontendHostname; + identity_number: IdentityNumber; +} +export type RegisterResponse = { + __kind__: "bad_challenge"; + /** + * The challenge was not successful. + */ + bad_challenge: null; +} | { + __kind__: "canister_full"; + /** + * No more registrations are possible in this instance of the II service canister. + */ + canister_full: null; +} | { + __kind__: "registered"; + /** + * A new user was successfully registered. + */ + registered: { + user_number: UserNumber; + }; +}; +export type HeaderField = [string, string]; +export type GetAttributesError = { + __kind__: "AuthorizationError"; + AuthorizationError: Principal; +} | { + __kind__: "ValidationError"; + ValidationError: { + problems: Array; + }; +} | { + __kind__: "GetAccountError"; + GetAccountError: GetAccountError; +}; +export type Timestamp = bigint; +export interface RateLimitConfig { + /** + * How many tokens are at most generated (to accommodate peaks). + */ + max_tokens: bigint; + /** + * Time it takes (in ns) for a rate limiting token to be replenished. + */ + time_per_token_ns: bigint; +} +export type IdentityInfoError = { + __kind__: "InternalCanisterError"; + /** + * Internal canister error. See the error message for details. + */ + InternalCanisterError: string; +} | { + __kind__: "Unauthorized"; + /** + * The principal is not authorized to call this method with the given arguments. + */ + Unauthorized: Principal; +}; +export type ChallengeKey = string; +export type AuthnMethodAddError = { + __kind__: "InvalidMetadata"; + InvalidMetadata: string; +}; +export type IdentityMetadataReplaceError = { + __kind__: "InternalCanisterError"; + /** + * Internal canister error. See the error message for details. + */ + InternalCanisterError: string; +} | { + __kind__: "Unauthorized"; + /** + * The principal is not authorized to call this method with the given arguments. + */ + Unauthorized: Principal; +} | { + __kind__: "StorageSpaceExceeded"; + /** + * The identity including the new metadata exceeds the maximum allowed size. + */ + StorageSpaceExceeded: { + space_required: bigint; + space_available: bigint; + }; +}; +export interface Challenge { + png_base64: string; + challenge_key: ChallengeKey; +} +export interface IdentityAuthnInfo { + authn_methods: Array; + recovery_authn_methods: Array; +} +export type PrepareAttributeError = { + __kind__: "AuthorizationError"; + AuthorizationError: Principal; +} | { + __kind__: "ValidationError"; + ValidationError: { + problems: Array; + }; +} | { + __kind__: "GetAccountError"; + GetAccountError: GetAccountError; +}; +export type GetIdAliasError = { + __kind__: "InternalCanisterError"; + /** + * Internal canister error. See the error message for details. + */ + InternalCanisterError: string; +} | { + __kind__: "Unauthorized"; + /** + * The principal is not authorized to call this method with the given arguments. + */ + Unauthorized: Principal; +} | { + __kind__: "NoSuchCredentials"; + /** + * The credential(s) are not available: may be expired or not prepared yet (call prepare_id_alias to prepare). + */ + NoSuchCredentials: string; +}; +export type Aud = string; +export type JWT = string; +export interface AccountInfo { + /** + * Configurable properties + */ + name?: string; + origin: string; + /** + * Null is unreserved default account + */ + account_number?: AccountNumber; + last_used?: Timestamp; +} +export interface AuthnMethodData { + security_settings: AuthnMethodSecuritySettings; + /** + * contains the following fields of the DeviceWithUsage type: + * - alias + * - origin + * - authenticator_attachment: data taken from key_type and reduced to "platform", "cross_platform" or absent on migration + * - usage: data taken from key_type and reduced to "recovery_phrase", "browser_storage_key" or absent on migration + * Note: for compatibility reasons with the v1 API, the entries above (if present) + * must be of the `String` variant. This restriction may be lifted in the future. + */ + metadata: MetadataMapV2; + last_authentication?: Timestamp; + authn_method: AuthnMethod; +} +export type PublicKey = Uint8Array; +export interface OpenIDRegFinishArg { + jwt: JWT; + name: string; + salt: Salt; +} +export type DeployArchiveResult = { + __kind__: "creation_in_progress"; + /** + * Initial archive creation is already in progress. + */ + creation_in_progress: null; +} | { + __kind__: "success"; + /** + * The archive was deployed successfully and the supplied wasm module has been installed. The principal of the archive + * canister is returned. + */ + success: Principal; +} | { + __kind__: "failed"; + /** + * Archive deployment failed. An error description is returned. + */ + failed: string; +}; +export interface OpenIdConfig { + auth_uri: string; + jwks_uri: string; + logo: string; + name: string; + fedcm_uri?: string; + email_verification?: OpenIdEmailVerification; + issuer: string; + auth_scope: Array; + client_id: string; +} +export type SessionKey = PublicKey; +export interface ChallengeResult { + key: ChallengeKey; + chars: string; +} +export type AuthnMethodRegistrationModeExitError = { + __kind__: "PasskeyWithThisPublicKeyIsAlreadyUsed"; + PasskeyWithThisPublicKeyIsAlreadyUsed: null; +} | { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "RegistrationModeOff"; + RegistrationModeOff: null; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +} | { + __kind__: "InvalidMetadata"; + InvalidMetadata: string; +}; +export type AuthnMethodRegistrationModeEnterError = { + __kind__: "InvalidRegistrationId"; + InvalidRegistrationId: string; +} | { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "AlreadyInProgress"; + AlreadyInProgress: null; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +}; +export interface OpenIdCredential { + aud: Aud; + iss: Iss; + sub: Sub; + metadata: MetadataMapV2; + last_usage_timestamp?: Timestamp; +} +export interface PrepareAccountDelegation { + user_key: UserKey; + expiration: Timestamp; +} +export type UserKey = PublicKey; +export type OpenIdCredentialRemoveError = { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "OpenIdCredentialNotFound"; + OpenIdCredentialNotFound: null; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +}; +export type Sub = string; +export interface HttpRequest { + url: string; + method: string; + body: Uint8Array; + headers: Array; + certificate_version?: number; +} +export enum AuthnMethodProtection { + Protected = "Protected", + Unprotected = "Unprotected" +} +export enum AuthnMethodPurpose { + Recovery = "Recovery", + Authentication = "Authentication" +} +export enum AuthnMethodSecuritySettingsReplaceError { + /** + * No authentication method found with the given public key. + */ + AuthnMethodNotFound = "AuthnMethodNotFound" +} +export enum DeviceProtection { + unprotected = "unprotected", + protected_ = "protected" +} +export enum IdRegStartError { + /** + * The method was called anonymously, which is not supported. + */ + InvalidCaller = "InvalidCaller", + /** + * A registration flow is already in progress. + */ + AlreadyInProgress = "AlreadyInProgress", + /** + * Too many registrations. Please try again later. + */ + RateLimitExceeded = "RateLimitExceeded" +} +export enum KeyType { + platform = "platform", + seed_phrase = "seed_phrase", + cross_platform = "cross_platform", + unknown_ = "unknown", + browser_storage_key = "browser_storage_key" +} +export enum OpenIdDelegationError { + NoSuchDelegation = "NoSuchDelegation", + NoSuchAnchor = "NoSuchAnchor", + JwtExpired = "JwtExpired", + JwtVerificationFailed = "JwtVerificationFailed" +} +export enum OpenIdEmailVerification { + Google = "Google", + Unknown = "Unknown", + Microsoft = "Microsoft" +} +export enum Purpose { + authentication = "authentication", + recovery = "recovery" +} +export enum Variant_CaptchaDisabled_CaptchaEnabled { + CaptchaDisabled = "CaptchaDisabled", + CaptchaEnabled = "CaptchaEnabled" +} +export enum Variant_Ok_Err { + Ok = "Ok", + Err = "Err" +} +export interface internet_identityInterface { + acknowledge_entries(sequence_number: bigint): Promise; + add(arg0: UserNumber, arg1: DeviceData): Promise; + add_tentative_device(arg0: UserNumber, arg1: DeviceData): Promise; + authn_method_add(arg0: IdentityNumber, arg1: AuthnMethodData): Promise<{ + __kind__: "Ok"; + Ok: null; + } | { + __kind__: "Err"; + Err: AuthnMethodAddError; + }>; + authn_method_confirm(arg0: IdentityNumber, confirmation_code: string): Promise<{ + __kind__: "Ok"; + Ok: null; + } | { + __kind__: "Err"; + Err: AuthnMethodConfirmationError; + }>; + authn_method_metadata_replace(arg0: IdentityNumber, arg1: PublicKey, arg2: MetadataMapV2): Promise<{ + __kind__: "Ok"; + Ok: null; + } | { + __kind__: "Err"; + Err: AuthnMethodMetadataReplaceError; + }>; + authn_method_register(arg0: IdentityNumber, arg1: AuthnMethodData): Promise<{ + __kind__: "Ok"; + Ok: AuthnMethodConfirmationCode; + } | { + __kind__: "Err"; + Err: AuthnMethodRegisterError; + }>; + authn_method_registration_mode_enter(arg0: IdentityNumber, arg1: RegistrationId | null): Promise<{ + __kind__: "Ok"; + Ok: { + expiration: Timestamp; + }; + } | { + __kind__: "Err"; + Err: AuthnMethodRegistrationModeEnterError; + }>; + authn_method_registration_mode_exit(arg0: IdentityNumber, arg1: AuthnMethodData | null): Promise<{ + __kind__: "Ok"; + Ok: null; + } | { + __kind__: "Err"; + Err: AuthnMethodRegistrationModeExitError; + }>; + authn_method_remove(arg0: IdentityNumber, arg1: PublicKey): Promise; + authn_method_replace(arg0: IdentityNumber, arg1: PublicKey, arg2: AuthnMethodData): Promise<{ + __kind__: "Ok"; + Ok: null; + } | { + __kind__: "Err"; + Err: AuthnMethodReplaceError; + }>; + authn_method_security_settings_replace(arg0: IdentityNumber, arg1: PublicKey, arg2: AuthnMethodSecuritySettings): Promise<{ + __kind__: "Ok"; + Ok: null; + } | { + __kind__: "Err"; + Err: AuthnMethodSecuritySettingsReplaceError; + }>; + authn_method_session_info(arg0: IdentityNumber): Promise; + authn_method_session_register(arg0: IdentityNumber): Promise<{ + __kind__: "Ok"; + Ok: AuthnMethodConfirmationCode; + } | { + __kind__: "Err"; + Err: AuthnMethodRegisterError; + }>; + check_captcha(arg0: CheckCaptchaArg): Promise<{ + __kind__: "Ok"; + Ok: IdRegNextStepResult; + } | { + __kind__: "Err"; + Err: CheckCaptchaError; + }>; + config(): Promise; + create_account(anchor_number: UserNumber, origin: FrontendHostname, name: string): Promise<{ + __kind__: "Ok"; + Ok: AccountInfo; + } | { + __kind__: "Err"; + Err: CreateAccountError; + }>; + create_challenge(): Promise; + deploy_archive(wasm: Uint8Array): Promise; + enter_device_registration_mode(arg0: UserNumber): Promise; + exit_device_registration_mode(arg0: UserNumber): Promise; + fetch_entries(): Promise>; + get_account_delegation(anchor_number: UserNumber, origin: FrontendHostname, account_number: AccountNumber | null, session_key: SessionKey, expiration: Timestamp): Promise<{ + __kind__: "Ok"; + Ok: SignedDelegation; + } | { + __kind__: "Err"; + Err: AccountDelegationError; + }>; + get_accounts(anchor_number: UserNumber, origin: FrontendHostname): Promise<{ + __kind__: "Ok"; + Ok: Array; + } | { + __kind__: "Err"; + Err: GetAccountsError; + }>; + get_anchor_credentials(arg0: UserNumber): Promise; + get_anchor_info(arg0: UserNumber): Promise; + get_attributes(arg0: GetAttributesRequest): Promise<{ + __kind__: "Ok"; + Ok: CertifiedAttributes; + } | { + __kind__: "Err"; + Err: GetAttributesError; + }>; + get_default_account(anchor_number: UserNumber, origin: FrontendHostname): Promise<{ + __kind__: "Ok"; + Ok: AccountInfo; + } | { + __kind__: "Err"; + Err: GetDefaultAccountError; + }>; + get_delegation(arg0: UserNumber, arg1: FrontendHostname, arg2: SessionKey, arg3: Timestamp): Promise; + get_id_alias(arg0: GetIdAliasRequest): Promise<{ + __kind__: "Ok"; + Ok: IdAliasCredentials; + } | { + __kind__: "Err"; + Err: GetIdAliasError; + }>; + get_principal(arg0: UserNumber, arg1: FrontendHostname): Promise; + http_request(request: HttpRequest): Promise; + identity_authn_info(arg0: IdentityNumber): Promise<{ + __kind__: "Ok"; + Ok: IdentityAuthnInfo; + } | { + __kind__: "Err"; + Err: null; + }>; + identity_info(arg0: IdentityNumber): Promise<{ + __kind__: "Ok"; + Ok: IdentityInfo; + } | { + __kind__: "Err"; + Err: IdentityInfoError; + }>; + identity_metadata_replace(arg0: IdentityNumber, arg1: MetadataMapV2): Promise<{ + __kind__: "Ok"; + Ok: null; + } | { + __kind__: "Err"; + Err: IdentityMetadataReplaceError; + }>; + identity_properties_replace(arg0: IdentityNumber, arg1: IdentityPropertiesReplace): Promise<{ + __kind__: "Ok"; + Ok: null; + } | { + __kind__: "Err"; + Err: IdentityPropertiesReplaceError; + }>; + identity_registration_finish(arg0: IdRegFinishArg): Promise<{ + __kind__: "Ok"; + Ok: IdRegFinishResult; + } | { + __kind__: "Err"; + Err: IdRegFinishError; + }>; + identity_registration_start(): Promise<{ + __kind__: "Ok"; + Ok: IdRegNextStepResult; + } | { + __kind__: "Err"; + Err: IdRegStartError; + }>; + init_salt(): Promise; + lookup(arg0: UserNumber): Promise>; + lookup_by_registration_mode_id(arg0: RegistrationId): Promise; + lookup_caller_identity_by_recovery_phrase(): Promise; + lookup_device_key(credential_id: Uint8Array): Promise; + openid_credential_add(arg0: IdentityNumber, arg1: JWT, arg2: Salt): Promise<{ + __kind__: "Ok"; + Ok: null; + } | { + __kind__: "Err"; + Err: OpenIdCredentialAddError; + }>; + openid_credential_remove(arg0: IdentityNumber, arg1: OpenIdCredentialKey): Promise<{ + __kind__: "Ok"; + Ok: null; + } | { + __kind__: "Err"; + Err: OpenIdCredentialRemoveError; + }>; + openid_get_delegation(arg0: JWT, arg1: Salt, arg2: SessionKey, arg3: Timestamp): Promise<{ + __kind__: "Ok"; + Ok: SignedDelegation; + } | { + __kind__: "Err"; + Err: OpenIdDelegationError; + }>; + openid_identity_registration_finish(arg0: OpenIDRegFinishArg): Promise<{ + __kind__: "Ok"; + Ok: IdRegFinishResult; + } | { + __kind__: "Err"; + Err: IdRegFinishError; + }>; + openid_prepare_delegation(arg0: JWT, arg1: Salt, arg2: SessionKey): Promise<{ + __kind__: "Ok"; + Ok: OpenIdPrepareDelegationResponse; + } | { + __kind__: "Err"; + Err: OpenIdDelegationError; + }>; + prepare_account_delegation(anchor_number: UserNumber, origin: FrontendHostname, account_number: AccountNumber | null, session_key: SessionKey, max_ttl: bigint | null): Promise<{ + __kind__: "Ok"; + Ok: PrepareAccountDelegation; + } | { + __kind__: "Err"; + Err: AccountDelegationError; + }>; + prepare_attributes(arg0: PrepareAttributeRequest): Promise<{ + __kind__: "Ok"; + Ok: PrepareAttributeResponse; + } | { + __kind__: "Err"; + Err: PrepareAttributeError; + }>; + prepare_delegation(arg0: UserNumber, arg1: FrontendHostname, arg2: SessionKey, maxTimeToLive: bigint | null): Promise<[UserKey, Timestamp]>; + prepare_id_alias(arg0: PrepareIdAliasRequest): Promise<{ + __kind__: "Ok"; + Ok: PreparedIdAlias; + } | { + __kind__: "Err"; + Err: PrepareIdAliasError; + }>; + register(arg0: DeviceData, arg1: ChallengeResult, arg2: Principal | null): Promise; + remove(arg0: UserNumber, arg1: DeviceKey): Promise; + replace(arg0: UserNumber, arg1: DeviceKey, arg2: DeviceData): Promise; + set_default_account(anchor_number: UserNumber, origin: FrontendHostname, account_number: AccountNumber | null): Promise<{ + __kind__: "Ok"; + Ok: AccountInfo; + } | { + __kind__: "Err"; + Err: SetDefaultAccountError; + }>; + stats(): Promise; + update(arg0: UserNumber, arg1: DeviceKey, arg2: DeviceData): Promise; + update_account(anchor_number: UserNumber, origin: FrontendHostname, account_number: AccountNumber | null, update: AccountUpdate): Promise<{ + __kind__: "Ok"; + Ok: AccountInfo; + } | { + __kind__: "Err"; + Err: UpdateAccountError; + }>; + verify_tentative_device(arg0: UserNumber, verification_code: string): Promise; +} +import type { Aaguid as _Aaguid, AccountDelegationError as _AccountDelegationError, AccountInfo as _AccountInfo, AccountNumber as _AccountNumber, AccountUpdate as _AccountUpdate, AddTentativeDeviceResponse as _AddTentativeDeviceResponse, AnalyticsConfig as _AnalyticsConfig, ArchiveConfig as _ArchiveConfig, ArchiveInfo as _ArchiveInfo, Aud as _Aud, AuthnMethod as _AuthnMethod, AuthnMethodAddError as _AuthnMethodAddError, AuthnMethodConfirmationCode as _AuthnMethodConfirmationCode, AuthnMethodConfirmationError as _AuthnMethodConfirmationError, AuthnMethodData as _AuthnMethodData, AuthnMethodMetadataReplaceError as _AuthnMethodMetadataReplaceError, AuthnMethodProtection as _AuthnMethodProtection, AuthnMethodPurpose as _AuthnMethodPurpose, AuthnMethodRegisterError as _AuthnMethodRegisterError, AuthnMethodRegistrationInfo as _AuthnMethodRegistrationInfo, AuthnMethodRegistrationModeEnterError as _AuthnMethodRegistrationModeEnterError, AuthnMethodRegistrationModeExitError as _AuthnMethodRegistrationModeExitError, AuthnMethodReplaceError as _AuthnMethodReplaceError, AuthnMethodSecuritySettings as _AuthnMethodSecuritySettings, AuthnMethodSecuritySettingsReplaceError as _AuthnMethodSecuritySettingsReplaceError, AuthnMethodSessionInfo as _AuthnMethodSessionInfo, CaptchaConfig as _CaptchaConfig, CertifiedAttributes as _CertifiedAttributes, CheckCaptchaError as _CheckCaptchaError, CreateAccountError as _CreateAccountError, CredentialId as _CredentialId, Delegation as _Delegation, DeployArchiveResult as _DeployArchiveResult, DeviceData as _DeviceData, DeviceKey as _DeviceKey, DeviceKeyWithAnchor as _DeviceKeyWithAnchor, DeviceProtection as _DeviceProtection, DeviceRegistrationInfo as _DeviceRegistrationInfo, DeviceWithUsage as _DeviceWithUsage, DummyAuthConfig as _DummyAuthConfig, FrontendHostname as _FrontendHostname, GetAccountError as _GetAccountError, GetAccountsError as _GetAccountsError, GetAttributesError as _GetAttributesError, GetAttributesRequest as _GetAttributesRequest, GetDefaultAccountError as _GetDefaultAccountError, GetDelegationResponse as _GetDelegationResponse, GetIdAliasError as _GetIdAliasError, HeaderField as _HeaderField, HttpRequest as _HttpRequest, HttpResponse as _HttpResponse, IdAliasCredentials as _IdAliasCredentials, IdRegFinishArg as _IdRegFinishArg, IdRegFinishError as _IdRegFinishError, IdRegFinishResult as _IdRegFinishResult, IdRegNextStepResult as _IdRegNextStepResult, IdRegStartError as _IdRegStartError, IdentityAnchorInfo as _IdentityAnchorInfo, IdentityAuthnInfo as _IdentityAuthnInfo, IdentityInfo as _IdentityInfo, IdentityInfoError as _IdentityInfoError, IdentityMetadataReplaceError as _IdentityMetadataReplaceError, IdentityNumber as _IdentityNumber, IdentityPropertiesReplace as _IdentityPropertiesReplace, IdentityPropertiesReplaceError as _IdentityPropertiesReplaceError, InternetIdentityInit as _InternetIdentityInit, InternetIdentityStats as _InternetIdentityStats, Iss as _Iss, KeyType as _KeyType, MetadataMap as _MetadataMap, MetadataMapV2 as _MetadataMapV2, OpenIdConfig as _OpenIdConfig, OpenIdCredential as _OpenIdCredential, OpenIdCredentialAddError as _OpenIdCredentialAddError, OpenIdCredentialRemoveError as _OpenIdCredentialRemoveError, OpenIdDelegationError as _OpenIdDelegationError, OpenIdEmailVerification as _OpenIdEmailVerification, OpenIdPrepareDelegationResponse as _OpenIdPrepareDelegationResponse, PrepareAccountDelegation as _PrepareAccountDelegation, PrepareAttributeError as _PrepareAttributeError, PrepareAttributeRequest as _PrepareAttributeRequest, PrepareAttributeResponse as _PrepareAttributeResponse, PrepareIdAliasError as _PrepareIdAliasError, PreparedIdAlias as _PreparedIdAlias, PublicKey as _PublicKey, PublicKeyAuthn as _PublicKeyAuthn, Purpose as _Purpose, RateLimitConfig as _RateLimitConfig, RegisterResponse as _RegisterResponse, RegistrationFlowNextStep as _RegistrationFlowNextStep, RegistrationId as _RegistrationId, SetDefaultAccountError as _SetDefaultAccountError, SignedDelegation as _SignedDelegation, Sub as _Sub, Timestamp as _Timestamp, UpdateAccountError as _UpdateAccountError, UserNumber as _UserNumber, VerifyTentativeDeviceResponse as _VerifyTentativeDeviceResponse, WebAuthn as _WebAuthn } from "./declarations/internet_identity.did.d.ts"; +export class Internet_identity implements internet_identityInterface { + constructor(private actor: ActorSubclass<_SERVICE>){} + async acknowledge_entries(arg0: bigint): Promise { + const result = await this.actor.acknowledge_entries(arg0); + return result; + } + async add(arg0: UserNumber, arg1: DeviceData): Promise { + const result = await this.actor.add(arg0, to_candid_DeviceData_n1(arg1)); + return result; + } + async add_tentative_device(arg0: UserNumber, arg1: DeviceData): Promise { + const result = await this.actor.add_tentative_device(arg0, to_candid_DeviceData_n1(arg1)); + return from_candid_AddTentativeDeviceResponse_n13(result); + } + async authn_method_add(arg0: IdentityNumber, arg1: AuthnMethodData): Promise<{ + __kind__: "Ok"; + Ok: null; + } | { + __kind__: "Err"; + Err: AuthnMethodAddError; + }> { + const result = await this.actor.authn_method_add(arg0, to_candid_AuthnMethodData_n15(arg1)); + return from_candid_variant_n31(result); + } + async authn_method_confirm(arg0: IdentityNumber, arg1: string): Promise<{ + __kind__: "Ok"; + Ok: null; + } | { + __kind__: "Err"; + Err: AuthnMethodConfirmationError; + }> { + const result = await this.actor.authn_method_confirm(arg0, arg1); + return from_candid_variant_n34(result); + } + async authn_method_metadata_replace(arg0: IdentityNumber, arg1: PublicKey, arg2: MetadataMapV2): Promise<{ + __kind__: "Ok"; + Ok: null; + } | { + __kind__: "Err"; + Err: AuthnMethodMetadataReplaceError; + }> { + const result = await this.actor.authn_method_metadata_replace(arg0, arg1, to_candid_MetadataMapV2_n23(arg2)); + return from_candid_variant_n37(result); + } + async authn_method_register(arg0: IdentityNumber, arg1: AuthnMethodData): Promise<{ + __kind__: "Ok"; + Ok: AuthnMethodConfirmationCode; + } | { + __kind__: "Err"; + Err: AuthnMethodRegisterError; + }> { + const result = await this.actor.authn_method_register(arg0, to_candid_AuthnMethodData_n15(arg1)); + return from_candid_variant_n40(result); + } + async authn_method_registration_mode_enter(arg0: IdentityNumber, arg1: RegistrationId | null): Promise<{ + __kind__: "Ok"; + Ok: { + expiration: Timestamp; + }; + } | { + __kind__: "Err"; + Err: AuthnMethodRegistrationModeEnterError; + }> { + const result = await this.actor.authn_method_registration_mode_enter(arg0, to_candid_opt_n43(arg1)); + return from_candid_variant_n44(result); + } + async authn_method_registration_mode_exit(arg0: IdentityNumber, arg1: AuthnMethodData | null): Promise<{ + __kind__: "Ok"; + Ok: null; + } | { + __kind__: "Err"; + Err: AuthnMethodRegistrationModeExitError; + }> { + const result = await this.actor.authn_method_registration_mode_exit(arg0, to_candid_opt_n47(arg1)); + return from_candid_variant_n48(result); + } + async authn_method_remove(arg0: IdentityNumber, arg1: PublicKey): Promise { + const result = await this.actor.authn_method_remove(arg0, arg1); + return from_candid_variant_n51(result); + } + async authn_method_replace(arg0: IdentityNumber, arg1: PublicKey, arg2: AuthnMethodData): Promise<{ + __kind__: "Ok"; + Ok: null; + } | { + __kind__: "Err"; + Err: AuthnMethodReplaceError; + }> { + const result = await this.actor.authn_method_replace(arg0, arg1, to_candid_AuthnMethodData_n15(arg2)); + return from_candid_variant_n52(result); + } + async authn_method_security_settings_replace(arg0: IdentityNumber, arg1: PublicKey, arg2: AuthnMethodSecuritySettings): Promise<{ + __kind__: "Ok"; + Ok: null; + } | { + __kind__: "Err"; + Err: AuthnMethodSecuritySettingsReplaceError; + }> { + const result = await this.actor.authn_method_security_settings_replace(arg0, arg1, to_candid_AuthnMethodSecuritySettings_n17(arg2)); + return from_candid_variant_n55(result); + } + async authn_method_session_info(arg0: IdentityNumber): Promise { + const result = await this.actor.authn_method_session_info(arg0); + return from_candid_opt_n58(result); + } + async authn_method_session_register(arg0: IdentityNumber): Promise<{ + __kind__: "Ok"; + Ok: AuthnMethodConfirmationCode; + } | { + __kind__: "Err"; + Err: AuthnMethodRegisterError; + }> { + const result = await this.actor.authn_method_session_register(arg0); + return from_candid_variant_n40(result); + } + async check_captcha(arg0: CheckCaptchaArg): Promise<{ + __kind__: "Ok"; + Ok: IdRegNextStepResult; + } | { + __kind__: "Err"; + Err: CheckCaptchaError; + }> { + const result = await this.actor.check_captcha(arg0); + return from_candid_variant_n63(result); + } + async config(): Promise { + const result = await this.actor.config(); + return from_candid_InternetIdentityInit_n70(result); + } + async create_account(arg0: UserNumber, arg1: FrontendHostname, arg2: string): Promise<{ + __kind__: "Ok"; + Ok: AccountInfo; + } | { + __kind__: "Err"; + Err: CreateAccountError; + }> { + const result = await this.actor.create_account(arg0, arg1, arg2); + return from_candid_variant_n98(result); + } + async create_challenge(): Promise { + const result = await this.actor.create_challenge(); + return result; + } + async deploy_archive(arg0: Uint8Array): Promise { + const result = await this.actor.deploy_archive(arg0); + return from_candid_DeployArchiveResult_n104(result); + } + async enter_device_registration_mode(arg0: UserNumber): Promise { + const result = await this.actor.enter_device_registration_mode(arg0); + return result; + } + async exit_device_registration_mode(arg0: UserNumber): Promise { + const result = await this.actor.exit_device_registration_mode(arg0); + return result; + } + async fetch_entries(): Promise> { + const result = await this.actor.fetch_entries(); + return result; + } + async get_account_delegation(arg0: UserNumber, arg1: FrontendHostname, arg2: AccountNumber | null, arg3: SessionKey, arg4: Timestamp): Promise<{ + __kind__: "Ok"; + Ok: SignedDelegation; + } | { + __kind__: "Err"; + Err: AccountDelegationError; + }> { + const result = await this.actor.get_account_delegation(arg0, arg1, to_candid_opt_n106(arg2), arg3, arg4); + return from_candid_variant_n107(result); + } + async get_accounts(arg0: UserNumber, arg1: FrontendHostname): Promise<{ + __kind__: "Ok"; + Ok: Array; + } | { + __kind__: "Err"; + Err: GetAccountsError; + }> { + const result = await this.actor.get_accounts(arg0, arg1); + return from_candid_variant_n115(result); + } + async get_anchor_credentials(arg0: UserNumber): Promise { + const result = await this.actor.get_anchor_credentials(arg0); + return result; + } + async get_anchor_info(arg0: UserNumber): Promise { + const result = await this.actor.get_anchor_info(arg0); + return from_candid_IdentityAnchorInfo_n119(result); + } + async get_attributes(arg0: GetAttributesRequest): Promise<{ + __kind__: "Ok"; + Ok: CertifiedAttributes; + } | { + __kind__: "Err"; + Err: GetAttributesError; + }> { + const result = await this.actor.get_attributes(to_candid_GetAttributesRequest_n151(arg0)); + return from_candid_variant_n153(result); + } + async get_default_account(arg0: UserNumber, arg1: FrontendHostname): Promise<{ + __kind__: "Ok"; + Ok: AccountInfo; + } | { + __kind__: "Err"; + Err: GetDefaultAccountError; + }> { + const result = await this.actor.get_default_account(arg0, arg1); + return from_candid_variant_n158(result); + } + async get_delegation(arg0: UserNumber, arg1: FrontendHostname, arg2: SessionKey, arg3: Timestamp): Promise { + const result = await this.actor.get_delegation(arg0, arg1, arg2, arg3); + return from_candid_GetDelegationResponse_n161(result); + } + async get_id_alias(arg0: GetIdAliasRequest): Promise<{ + __kind__: "Ok"; + Ok: IdAliasCredentials; + } | { + __kind__: "Err"; + Err: GetIdAliasError; + }> { + const result = await this.actor.get_id_alias(arg0); + return from_candid_variant_n163(result); + } + async get_principal(arg0: UserNumber, arg1: FrontendHostname): Promise { + const result = await this.actor.get_principal(arg0, arg1); + return result; + } + async http_request(arg0: HttpRequest): Promise { + const result = await this.actor.http_request(to_candid_HttpRequest_n166(arg0)); + return from_candid_HttpResponse_n168(result); + } + async identity_authn_info(arg0: IdentityNumber): Promise<{ + __kind__: "Ok"; + Ok: IdentityAuthnInfo; + } | { + __kind__: "Err"; + Err: null; + }> { + const result = await this.actor.identity_authn_info(arg0); + return from_candid_variant_n170(result); + } + async identity_info(arg0: IdentityNumber): Promise<{ + __kind__: "Ok"; + Ok: IdentityInfo; + } | { + __kind__: "Err"; + Err: IdentityInfoError; + }> { + const result = await this.actor.identity_info(arg0); + return from_candid_variant_n178(result); + } + async identity_metadata_replace(arg0: IdentityNumber, arg1: MetadataMapV2): Promise<{ + __kind__: "Ok"; + Ok: null; + } | { + __kind__: "Err"; + Err: IdentityMetadataReplaceError; + }> { + const result = await this.actor.identity_metadata_replace(arg0, to_candid_MetadataMapV2_n23(arg1)); + return from_candid_variant_n195(result); + } + async identity_properties_replace(arg0: IdentityNumber, arg1: IdentityPropertiesReplace): Promise<{ + __kind__: "Ok"; + Ok: null; + } | { + __kind__: "Err"; + Err: IdentityPropertiesReplaceError; + }> { + const result = await this.actor.identity_properties_replace(arg0, to_candid_IdentityPropertiesReplace_n198(arg1)); + return from_candid_variant_n200(result); + } + async identity_registration_finish(arg0: IdRegFinishArg): Promise<{ + __kind__: "Ok"; + Ok: IdRegFinishResult; + } | { + __kind__: "Err"; + Err: IdRegFinishError; + }> { + const result = await this.actor.identity_registration_finish(to_candid_IdRegFinishArg_n203(arg0)); + return from_candid_variant_n205(result); + } + async identity_registration_start(): Promise<{ + __kind__: "Ok"; + Ok: IdRegNextStepResult; + } | { + __kind__: "Err"; + Err: IdRegStartError; + }> { + const result = await this.actor.identity_registration_start(); + return from_candid_variant_n208(result); + } + async init_salt(): Promise { + const result = await this.actor.init_salt(); + return result; + } + async lookup(arg0: UserNumber): Promise> { + const result = await this.actor.lookup(arg0); + return from_candid_vec_n211(result); + } + async lookup_by_registration_mode_id(arg0: RegistrationId): Promise { + const result = await this.actor.lookup_by_registration_mode_id(arg0); + return from_candid_opt_n212(result); + } + async lookup_caller_identity_by_recovery_phrase(): Promise { + const result = await this.actor.lookup_caller_identity_by_recovery_phrase(); + return from_candid_opt_n212(result); + } + async lookup_device_key(arg0: Uint8Array): Promise { + const result = await this.actor.lookup_device_key(arg0); + return from_candid_opt_n213(result); + } + async openid_credential_add(arg0: IdentityNumber, arg1: JWT, arg2: Salt): Promise<{ + __kind__: "Ok"; + Ok: null; + } | { + __kind__: "Err"; + Err: OpenIdCredentialAddError; + }> { + const result = await this.actor.openid_credential_add(arg0, arg1, arg2); + return from_candid_variant_n214(result); + } + async openid_credential_remove(arg0: IdentityNumber, arg1: OpenIdCredentialKey): Promise<{ + __kind__: "Ok"; + Ok: null; + } | { + __kind__: "Err"; + Err: OpenIdCredentialRemoveError; + }> { + const result = await this.actor.openid_credential_remove(arg0, arg1); + return from_candid_variant_n217(result); + } + async openid_get_delegation(arg0: JWT, arg1: Salt, arg2: SessionKey, arg3: Timestamp): Promise<{ + __kind__: "Ok"; + Ok: SignedDelegation; + } | { + __kind__: "Err"; + Err: OpenIdDelegationError; + }> { + const result = await this.actor.openid_get_delegation(arg0, arg1, arg2, arg3); + return from_candid_variant_n220(result); + } + async openid_identity_registration_finish(arg0: OpenIDRegFinishArg): Promise<{ + __kind__: "Ok"; + Ok: IdRegFinishResult; + } | { + __kind__: "Err"; + Err: IdRegFinishError; + }> { + const result = await this.actor.openid_identity_registration_finish(arg0); + return from_candid_variant_n205(result); + } + async openid_prepare_delegation(arg0: JWT, arg1: Salt, arg2: SessionKey): Promise<{ + __kind__: "Ok"; + Ok: OpenIdPrepareDelegationResponse; + } | { + __kind__: "Err"; + Err: OpenIdDelegationError; + }> { + const result = await this.actor.openid_prepare_delegation(arg0, arg1, arg2); + return from_candid_variant_n223(result); + } + async prepare_account_delegation(arg0: UserNumber, arg1: FrontendHostname, arg2: AccountNumber | null, arg3: SessionKey, arg4: bigint | null): Promise<{ + __kind__: "Ok"; + Ok: PrepareAccountDelegation; + } | { + __kind__: "Err"; + Err: AccountDelegationError; + }> { + const result = await this.actor.prepare_account_delegation(arg0, arg1, to_candid_opt_n106(arg2), arg3, to_candid_opt_n224(arg4)); + return from_candid_variant_n225(result); + } + async prepare_attributes(arg0: PrepareAttributeRequest): Promise<{ + __kind__: "Ok"; + Ok: PrepareAttributeResponse; + } | { + __kind__: "Err"; + Err: PrepareAttributeError; + }> { + const result = await this.actor.prepare_attributes(to_candid_PrepareAttributeRequest_n226(arg0)); + return from_candid_variant_n228(result); + } + async prepare_delegation(arg0: UserNumber, arg1: FrontendHostname, arg2: SessionKey, arg3: bigint | null): Promise<[UserKey, Timestamp]> { + const result = await this.actor.prepare_delegation(arg0, arg1, arg2, to_candid_opt_n224(arg3)); + return [ + result[0], + result[1] + ]; + } + async prepare_id_alias(arg0: PrepareIdAliasRequest): Promise<{ + __kind__: "Ok"; + Ok: PreparedIdAlias; + } | { + __kind__: "Err"; + Err: PrepareIdAliasError; + }> { + const result = await this.actor.prepare_id_alias(arg0); + return from_candid_variant_n230(result); + } + async register(arg0: DeviceData, arg1: ChallengeResult, arg2: Principal | null): Promise { + const result = await this.actor.register(to_candid_DeviceData_n1(arg0), arg1, to_candid_opt_n232(arg2)); + return from_candid_RegisterResponse_n233(result); + } + async remove(arg0: UserNumber, arg1: DeviceKey): Promise { + const result = await this.actor.remove(arg0, arg1); + return result; + } + async replace(arg0: UserNumber, arg1: DeviceKey, arg2: DeviceData): Promise { + const result = await this.actor.replace(arg0, arg1, to_candid_DeviceData_n1(arg2)); + return result; + } + async set_default_account(arg0: UserNumber, arg1: FrontendHostname, arg2: AccountNumber | null): Promise<{ + __kind__: "Ok"; + Ok: AccountInfo; + } | { + __kind__: "Err"; + Err: SetDefaultAccountError; + }> { + const result = await this.actor.set_default_account(arg0, arg1, to_candid_opt_n106(arg2)); + return from_candid_variant_n235(result); + } + async stats(): Promise { + const result = await this.actor.stats(); + return from_candid_InternetIdentityStats_n238(result); + } + async update(arg0: UserNumber, arg1: DeviceKey, arg2: DeviceData): Promise { + const result = await this.actor.update(arg0, arg1, to_candid_DeviceData_n1(arg2)); + return result; + } + async update_account(arg0: UserNumber, arg1: FrontendHostname, arg2: AccountNumber | null, arg3: AccountUpdate): Promise<{ + __kind__: "Ok"; + Ok: AccountInfo; + } | { + __kind__: "Err"; + Err: UpdateAccountError; + }> { + const result = await this.actor.update_account(arg0, arg1, to_candid_opt_n106(arg2), to_candid_AccountUpdate_n242(arg3)); + return from_candid_variant_n243(result); + } + async verify_tentative_device(arg0: UserNumber, arg1: string): Promise { + const result = await this.actor.verify_tentative_device(arg0, arg1); + return from_candid_VerifyTentativeDeviceResponse_n245(result); + } +} +function from_candid_AccountDelegationError_n113(value: _AccountDelegationError): AccountDelegationError { + return from_candid_variant_n114(value); +} +function from_candid_AccountInfo_n99(value: _AccountInfo): AccountInfo { + return from_candid_record_n100(value); +} +function from_candid_AddTentativeDeviceResponse_n13(value: _AddTentativeDeviceResponse): AddTentativeDeviceResponse { + return from_candid_variant_n14(value); +} +function from_candid_AnalyticsConfig_n80(value: _AnalyticsConfig): AnalyticsConfig { + return from_candid_variant_n81(value); +} +function from_candid_ArchiveInfo_n240(value: _ArchiveInfo): ArchiveInfo { + return from_candid_record_n241(value); +} +function from_candid_AuthnMethodAddError_n32(value: _AuthnMethodAddError): AuthnMethodAddError { + return from_candid_variant_n33(value); +} +function from_candid_AuthnMethodConfirmationError_n35(value: _AuthnMethodConfirmationError): AuthnMethodConfirmationError { + return from_candid_variant_n36(value); +} +function from_candid_AuthnMethodData_n182(value: _AuthnMethodData): AuthnMethodData { + return from_candid_record_n183(value); +} +function from_candid_AuthnMethodMetadataReplaceError_n38(value: _AuthnMethodMetadataReplaceError): AuthnMethodMetadataReplaceError { + return from_candid_variant_n39(value); +} +function from_candid_AuthnMethodProtection_n186(value: _AuthnMethodProtection): AuthnMethodProtection { + return from_candid_variant_n187(value); +} +function from_candid_AuthnMethodPurpose_n188(value: _AuthnMethodPurpose): AuthnMethodPurpose { + return from_candid_variant_n189(value); +} +function from_candid_AuthnMethodRegisterError_n41(value: _AuthnMethodRegisterError): AuthnMethodRegisterError { + return from_candid_variant_n42(value); +} +function from_candid_AuthnMethodRegistrationInfo_n191(value: _AuthnMethodRegistrationInfo): AuthnMethodRegistrationInfo { + return from_candid_record_n192(value); +} +function from_candid_AuthnMethodRegistrationModeEnterError_n45(value: _AuthnMethodRegistrationModeEnterError): AuthnMethodRegistrationModeEnterError { + return from_candid_variant_n46(value); +} +function from_candid_AuthnMethodRegistrationModeExitError_n49(value: _AuthnMethodRegistrationModeExitError): AuthnMethodRegistrationModeExitError { + return from_candid_variant_n50(value); +} +function from_candid_AuthnMethodReplaceError_n53(value: _AuthnMethodReplaceError): AuthnMethodReplaceError { + return from_candid_variant_n54(value); +} +function from_candid_AuthnMethodSecuritySettingsReplaceError_n56(value: _AuthnMethodSecuritySettingsReplaceError): AuthnMethodSecuritySettingsReplaceError { + return from_candid_variant_n57(value); +} +function from_candid_AuthnMethodSecuritySettings_n184(value: _AuthnMethodSecuritySettings): AuthnMethodSecuritySettings { + return from_candid_record_n185(value); +} +function from_candid_AuthnMethodSessionInfo_n59(value: _AuthnMethodSessionInfo): AuthnMethodSessionInfo { + return from_candid_record_n60(value); +} +function from_candid_AuthnMethod_n174(value: _AuthnMethod): AuthnMethod { + return from_candid_variant_n175(value); +} +function from_candid_CaptchaConfig_n91(value: _CaptchaConfig): CaptchaConfig { + return from_candid_record_n92(value); +} +function from_candid_CheckCaptchaError_n68(value: _CheckCaptchaError): CheckCaptchaError { + return from_candid_variant_n69(value); +} +function from_candid_CreateAccountError_n102(value: _CreateAccountError): CreateAccountError { + return from_candid_variant_n103(value); +} +function from_candid_Delegation_n110(value: _Delegation): Delegation { + return from_candid_record_n111(value); +} +function from_candid_DeployArchiveResult_n104(value: _DeployArchiveResult): DeployArchiveResult { + return from_candid_variant_n105(value); +} +function from_candid_DeviceData_n149(value: _DeviceData): DeviceData { + return from_candid_record_n150(value); +} +function from_candid_DeviceProtection_n129(value: _DeviceProtection): DeviceProtection { + return from_candid_variant_n130(value); +} +function from_candid_DeviceRegistrationInfo_n146(value: _DeviceRegistrationInfo): DeviceRegistrationInfo { + return from_candid_record_n147(value); +} +function from_candid_DeviceWithUsage_n122(value: _DeviceWithUsage): DeviceWithUsage { + return from_candid_record_n123(value); +} +function from_candid_GetAccountError_n156(value: _GetAccountError): GetAccountError { + return from_candid_variant_n157(value); +} +function from_candid_GetAccountsError_n117(value: _GetAccountsError): GetAccountsError { + return from_candid_variant_n118(value); +} +function from_candid_GetAttributesError_n154(value: _GetAttributesError): GetAttributesError { + return from_candid_variant_n155(value); +} +function from_candid_GetDefaultAccountError_n159(value: _GetDefaultAccountError): GetDefaultAccountError { + return from_candid_variant_n160(value); +} +function from_candid_GetDelegationResponse_n161(value: _GetDelegationResponse): GetDelegationResponse { + return from_candid_variant_n162(value); +} +function from_candid_GetIdAliasError_n164(value: _GetIdAliasError): GetIdAliasError { + return from_candid_variant_n165(value); +} +function from_candid_HttpResponse_n168(value: _HttpResponse): HttpResponse { + return from_candid_record_n169(value); +} +function from_candid_IdRegFinishError_n206(value: _IdRegFinishError): IdRegFinishError { + return from_candid_variant_n207(value); +} +function from_candid_IdRegNextStepResult_n64(value: _IdRegNextStepResult): IdRegNextStepResult { + return from_candid_record_n65(value); +} +function from_candid_IdRegStartError_n209(value: _IdRegStartError): IdRegStartError { + return from_candid_variant_n210(value); +} +function from_candid_IdentityAnchorInfo_n119(value: _IdentityAnchorInfo): IdentityAnchorInfo { + return from_candid_record_n120(value); +} +function from_candid_IdentityAuthnInfo_n171(value: _IdentityAuthnInfo): IdentityAuthnInfo { + return from_candid_record_n172(value); +} +function from_candid_IdentityInfoError_n194(value: _IdentityInfoError): IdentityInfoError { + return from_candid_variant_n118(value); +} +function from_candid_IdentityInfo_n179(value: _IdentityInfo): IdentityInfo { + return from_candid_record_n180(value); +} +function from_candid_IdentityMetadataReplaceError_n196(value: _IdentityMetadataReplaceError): IdentityMetadataReplaceError { + return from_candid_variant_n197(value); +} +function from_candid_IdentityPropertiesReplaceError_n201(value: _IdentityPropertiesReplaceError): IdentityPropertiesReplaceError { + return from_candid_variant_n202(value); +} +function from_candid_InternetIdentityInit_n70(value: _InternetIdentityInit): InternetIdentityInit { + return from_candid_record_n71(value); +} +function from_candid_InternetIdentityStats_n238(value: _InternetIdentityStats): InternetIdentityStats { + return from_candid_record_n239(value); +} +function from_candid_KeyType_n131(value: _KeyType): KeyType { + return from_candid_variant_n132(value); +} +function from_candid_MetadataMapV2_n141(value: _MetadataMapV2): MetadataMapV2 { + return from_candid_vec_n142(value); +} +function from_candid_MetadataMap_n125(value: _MetadataMap): MetadataMap { + return from_candid_vec_n126(value); +} +function from_candid_OpenIdConfig_n85(value: _OpenIdConfig): OpenIdConfig { + return from_candid_record_n86(value); +} +function from_candid_OpenIdCredentialAddError_n215(value: _OpenIdCredentialAddError): OpenIdCredentialAddError { + return from_candid_variant_n216(value); +} +function from_candid_OpenIdCredentialRemoveError_n218(value: _OpenIdCredentialRemoveError): OpenIdCredentialRemoveError { + return from_candid_variant_n219(value); +} +function from_candid_OpenIdCredential_n139(value: _OpenIdCredential): OpenIdCredential { + return from_candid_record_n140(value); +} +function from_candid_OpenIdDelegationError_n221(value: _OpenIdDelegationError): OpenIdDelegationError { + return from_candid_variant_n222(value); +} +function from_candid_OpenIdEmailVerification_n88(value: _OpenIdEmailVerification): OpenIdEmailVerification { + return from_candid_variant_n89(value); +} +function from_candid_PrepareAttributeError_n229(value: _PrepareAttributeError): PrepareAttributeError { + return from_candid_variant_n155(value); +} +function from_candid_PrepareIdAliasError_n231(value: _PrepareIdAliasError): PrepareIdAliasError { + return from_candid_variant_n118(value); +} +function from_candid_Purpose_n134(value: _Purpose): Purpose { + return from_candid_variant_n135(value); +} +function from_candid_RegisterResponse_n233(value: _RegisterResponse): RegisterResponse { + return from_candid_variant_n234(value); +} +function from_candid_RegistrationFlowNextStep_n66(value: _RegistrationFlowNextStep): RegistrationFlowNextStep { + return from_candid_variant_n67(value); +} +function from_candid_SetDefaultAccountError_n236(value: _SetDefaultAccountError): SetDefaultAccountError { + return from_candid_variant_n237(value); +} +function from_candid_SignedDelegation_n108(value: _SignedDelegation): SignedDelegation { + return from_candid_record_n109(value); +} +function from_candid_UpdateAccountError_n244(value: _UpdateAccountError): UpdateAccountError { + return from_candid_variant_n103(value); +} +function from_candid_VerifyTentativeDeviceResponse_n245(value: _VerifyTentativeDeviceResponse): VerifyTentativeDeviceResponse { + return from_candid_variant_n246(value); +} +function from_candid_WebAuthn_n176(value: _WebAuthn): WebAuthn { + return from_candid_record_n177(value); +} +function from_candid_opt_n101(value: [] | [_AccountNumber]): AccountNumber | null { + return value.length === 0 ? null : value[0]; +} +function from_candid_opt_n112(value: [] | [Array]): Array | null { + return value.length === 0 ? null : value[0]; +} +function from_candid_opt_n124(value: [] | [_MetadataMap]): MetadataMap | null { + return value.length === 0 ? null : from_candid_MetadataMap_n125(value[0]); +} +function from_candid_opt_n133(value: [] | [_Aaguid]): Aaguid | null { + return value.length === 0 ? null : value[0]; +} +function from_candid_opt_n136(value: [] | [_CredentialId]): CredentialId | null { + return value.length === 0 ? null : value[0]; +} +function from_candid_opt_n137(value: [] | [Array<_OpenIdCredential>]): Array | null { + return value.length === 0 ? null : from_candid_vec_n138(value[0]); +} +function from_candid_opt_n145(value: [] | [_DeviceRegistrationInfo]): DeviceRegistrationInfo | null { + return value.length === 0 ? null : from_candid_DeviceRegistrationInfo_n146(value[0]); +} +function from_candid_opt_n148(value: [] | [_DeviceData]): DeviceData | null { + return value.length === 0 ? null : from_candid_DeviceData_n149(value[0]); +} +function from_candid_opt_n190(value: [] | [_AuthnMethodRegistrationInfo]): AuthnMethodRegistrationInfo | null { + return value.length === 0 ? null : from_candid_AuthnMethodRegistrationInfo_n191(value[0]); +} +function from_candid_opt_n193(value: [] | [_AuthnMethodData]): AuthnMethodData | null { + return value.length === 0 ? null : from_candid_AuthnMethodData_n182(value[0]); +} +function from_candid_opt_n212(value: [] | [_IdentityNumber]): IdentityNumber | null { + return value.length === 0 ? null : value[0]; +} +function from_candid_opt_n213(value: [] | [_DeviceKeyWithAnchor]): DeviceKeyWithAnchor | null { + return value.length === 0 ? null : value[0]; +} +function from_candid_opt_n58(value: [] | [_AuthnMethodSessionInfo]): AuthnMethodSessionInfo | null { + return value.length === 0 ? null : from_candid_AuthnMethodSessionInfo_n59(value[0]); +} +function from_candid_opt_n61(value: [] | [string]): string | null { + return value.length === 0 ? null : value[0]; +} +function from_candid_opt_n62(value: [] | [_Timestamp]): Timestamp | null { + return value.length === 0 ? null : value[0]; +} +function from_candid_opt_n72(value: [] | [boolean]): boolean | null { + return value.length === 0 ? null : value[0]; +} +function from_candid_opt_n73(value: [] | [Principal]): Principal | null { + return value.length === 0 ? null : value[0]; +} +function from_candid_opt_n74(value: [] | [[bigint, bigint]]): [bigint, bigint] | null { + return value.length === 0 ? null : value[0]; +} +function from_candid_opt_n75(value: [] | [Array]): Array | null { + return value.length === 0 ? null : value[0]; +} +function from_candid_opt_n76(value: [] | [_ArchiveConfig]): ArchiveConfig | null { + return value.length === 0 ? null : value[0]; +} +function from_candid_opt_n77(value: [] | [bigint]): bigint | null { + return value.length === 0 ? null : value[0]; +} +function from_candid_opt_n78(value: [] | [[] | [_AnalyticsConfig]]): Some | None { + return value.length === 0 ? none() : some(from_candid_opt_n79(value[0])); +} +function from_candid_opt_n79(value: [] | [_AnalyticsConfig]): AnalyticsConfig | null { + return value.length === 0 ? null : from_candid_AnalyticsConfig_n80(value[0]); +} +function from_candid_opt_n83(value: [] | [Array<_OpenIdConfig>]): Array | null { + return value.length === 0 ? null : from_candid_vec_n84(value[0]); +} +function from_candid_opt_n87(value: [] | [_OpenIdEmailVerification]): OpenIdEmailVerification | null { + return value.length === 0 ? null : from_candid_OpenIdEmailVerification_n88(value[0]); +} +function from_candid_opt_n90(value: [] | [_CaptchaConfig]): CaptchaConfig | null { + return value.length === 0 ? null : from_candid_CaptchaConfig_n91(value[0]); +} +function from_candid_opt_n95(value: [] | [[] | [_DummyAuthConfig]]): Some | None { + return value.length === 0 ? none() : some(from_candid_opt_n96(value[0])); +} +function from_candid_opt_n96(value: [] | [_DummyAuthConfig]): DummyAuthConfig | null { + return value.length === 0 ? null : value[0]; +} +function from_candid_opt_n97(value: [] | [_RateLimitConfig]): RateLimitConfig | null { + return value.length === 0 ? null : value[0]; +} +function from_candid_record_n100(value: { + name: [] | [string]; + origin: string; + account_number: [] | [_AccountNumber]; + last_used: [] | [_Timestamp]; +}): { + name?: string; + origin: string; + account_number?: AccountNumber; + last_used?: Timestamp; +} { + return { + name: record_opt_to_undefined(from_candid_opt_n61(value.name)), + origin: value.origin, + account_number: record_opt_to_undefined(from_candid_opt_n101(value.account_number)), + last_used: record_opt_to_undefined(from_candid_opt_n62(value.last_used)) + }; +} +function from_candid_record_n109(value: { + signature: Uint8Array; + delegation: _Delegation; +}): { + signature: Uint8Array; + delegation: Delegation; +} { + return { + signature: value.signature, + delegation: from_candid_Delegation_n110(value.delegation) + }; +} +function from_candid_record_n111(value: { + pubkey: _PublicKey; + targets: [] | [Array]; + expiration: _Timestamp; +}): { + pubkey: PublicKey; + targets?: Array; + expiration: Timestamp; +} { + return { + pubkey: value.pubkey, + targets: record_opt_to_undefined(from_candid_opt_n112(value.targets)), + expiration: value.expiration + }; +} +function from_candid_record_n120(value: { + name: [] | [string]; + created_at: [] | [_Timestamp]; + devices: Array<_DeviceWithUsage>; + openid_credentials: [] | [Array<_OpenIdCredential>]; + device_registration: [] | [_DeviceRegistrationInfo]; +}): { + name?: string; + created_at?: Timestamp; + devices: Array; + openid_credentials?: Array; + device_registration?: DeviceRegistrationInfo; +} { + return { + name: record_opt_to_undefined(from_candid_opt_n61(value.name)), + created_at: record_opt_to_undefined(from_candid_opt_n62(value.created_at)), + devices: from_candid_vec_n121(value.devices), + openid_credentials: record_opt_to_undefined(from_candid_opt_n137(value.openid_credentials)), + device_registration: record_opt_to_undefined(from_candid_opt_n145(value.device_registration)) + }; +} +function from_candid_record_n123(value: { + alias: string; + last_usage: [] | [_Timestamp]; + metadata: [] | [_MetadataMap]; + origin: [] | [string]; + protection: _DeviceProtection; + pubkey: _DeviceKey; + key_type: _KeyType; + aaguid: [] | [_Aaguid]; + purpose: _Purpose; + credential_id: [] | [_CredentialId]; +}): { + alias: string; + last_usage?: Timestamp; + metadata?: MetadataMap; + origin?: string; + protection: DeviceProtection; + pubkey: DeviceKey; + key_type: KeyType; + aaguid?: Aaguid; + purpose: Purpose; + credential_id?: CredentialId; +} { + return { + alias: value.alias, + last_usage: record_opt_to_undefined(from_candid_opt_n62(value.last_usage)), + metadata: record_opt_to_undefined(from_candid_opt_n124(value.metadata)), + origin: record_opt_to_undefined(from_candid_opt_n61(value.origin)), + protection: from_candid_DeviceProtection_n129(value.protection), + pubkey: value.pubkey, + key_type: from_candid_KeyType_n131(value.key_type), + aaguid: record_opt_to_undefined(from_candid_opt_n133(value.aaguid)), + purpose: from_candid_Purpose_n134(value.purpose), + credential_id: record_opt_to_undefined(from_candid_opt_n136(value.credential_id)) + }; +} +function from_candid_record_n140(value: { + aud: _Aud; + iss: _Iss; + sub: _Sub; + metadata: _MetadataMapV2; + last_usage_timestamp: [] | [_Timestamp]; +}): { + aud: Aud; + iss: Iss; + sub: Sub; + metadata: MetadataMapV2; + last_usage_timestamp?: Timestamp; +} { + return { + aud: value.aud, + iss: value.iss, + sub: value.sub, + metadata: from_candid_MetadataMapV2_n141(value.metadata), + last_usage_timestamp: record_opt_to_undefined(from_candid_opt_n62(value.last_usage_timestamp)) + }; +} +function from_candid_record_n147(value: { + tentative_device: [] | [_DeviceData]; + expiration: _Timestamp; + tentative_session: [] | [Principal]; +}): { + tentative_device?: DeviceData; + expiration: Timestamp; + tentative_session?: Principal; +} { + return { + tentative_device: record_opt_to_undefined(from_candid_opt_n148(value.tentative_device)), + expiration: value.expiration, + tentative_session: record_opt_to_undefined(from_candid_opt_n73(value.tentative_session)) + }; +} +function from_candid_record_n150(value: { + alias: string; + metadata: [] | [_MetadataMap]; + origin: [] | [string]; + protection: _DeviceProtection; + pubkey: _DeviceKey; + key_type: _KeyType; + aaguid: [] | [_Aaguid]; + purpose: _Purpose; + credential_id: [] | [_CredentialId]; +}): { + alias: string; + metadata?: MetadataMap; + origin?: string; + protection: DeviceProtection; + pubkey: DeviceKey; + key_type: KeyType; + aaguid?: Aaguid; + purpose: Purpose; + credential_id?: CredentialId; +} { + return { + alias: value.alias, + metadata: record_opt_to_undefined(from_candid_opt_n124(value.metadata)), + origin: record_opt_to_undefined(from_candid_opt_n61(value.origin)), + protection: from_candid_DeviceProtection_n129(value.protection), + pubkey: value.pubkey, + key_type: from_candid_KeyType_n131(value.key_type), + aaguid: record_opt_to_undefined(from_candid_opt_n133(value.aaguid)), + purpose: from_candid_Purpose_n134(value.purpose), + credential_id: record_opt_to_undefined(from_candid_opt_n136(value.credential_id)) + }; +} +function from_candid_record_n169(value: { + body: Uint8Array; + headers: Array<_HeaderField>; + upgrade: [] | [boolean]; + status_code: number; +}): { + body: Uint8Array; + headers: Array; + upgrade?: boolean; + status_code: number; +} { + return { + body: value.body, + headers: value.headers, + upgrade: record_opt_to_undefined(from_candid_opt_n72(value.upgrade)), + status_code: value.status_code + }; +} +function from_candid_record_n172(value: { + authn_methods: Array<_AuthnMethod>; + recovery_authn_methods: Array<_AuthnMethod>; +}): { + authn_methods: Array; + recovery_authn_methods: Array; +} { + return { + authn_methods: from_candid_vec_n173(value.authn_methods), + recovery_authn_methods: from_candid_vec_n173(value.recovery_authn_methods) + }; +} +function from_candid_record_n177(value: { + pubkey: _PublicKey; + aaguid: [] | [_Aaguid]; + credential_id: _CredentialId; +}): { + pubkey: PublicKey; + aaguid?: Aaguid; + credential_id: CredentialId; +} { + return { + pubkey: value.pubkey, + aaguid: record_opt_to_undefined(from_candid_opt_n133(value.aaguid)), + credential_id: value.credential_id + }; +} +function from_candid_record_n180(value: { + authn_methods: Array<_AuthnMethodData>; + metadata: _MetadataMapV2; + name: [] | [string]; + created_at: [] | [_Timestamp]; + authn_method_registration: [] | [_AuthnMethodRegistrationInfo]; + openid_credentials: [] | [Array<_OpenIdCredential>]; +}): { + authn_methods: Array; + metadata: MetadataMapV2; + name?: string; + created_at?: Timestamp; + authn_method_registration?: AuthnMethodRegistrationInfo; + openid_credentials?: Array; +} { + return { + authn_methods: from_candid_vec_n181(value.authn_methods), + metadata: from_candid_MetadataMapV2_n141(value.metadata), + name: record_opt_to_undefined(from_candid_opt_n61(value.name)), + created_at: record_opt_to_undefined(from_candid_opt_n62(value.created_at)), + authn_method_registration: record_opt_to_undefined(from_candid_opt_n190(value.authn_method_registration)), + openid_credentials: record_opt_to_undefined(from_candid_opt_n137(value.openid_credentials)) + }; +} +function from_candid_record_n183(value: { + security_settings: _AuthnMethodSecuritySettings; + metadata: _MetadataMapV2; + last_authentication: [] | [_Timestamp]; + authn_method: _AuthnMethod; +}): { + security_settings: AuthnMethodSecuritySettings; + metadata: MetadataMapV2; + last_authentication?: Timestamp; + authn_method: AuthnMethod; +} { + return { + security_settings: from_candid_AuthnMethodSecuritySettings_n184(value.security_settings), + metadata: from_candid_MetadataMapV2_n141(value.metadata), + last_authentication: record_opt_to_undefined(from_candid_opt_n62(value.last_authentication)), + authn_method: from_candid_AuthnMethod_n174(value.authn_method) + }; +} +function from_candid_record_n185(value: { + protection: _AuthnMethodProtection; + purpose: _AuthnMethodPurpose; +}): { + protection: AuthnMethodProtection; + purpose: AuthnMethodPurpose; +} { + return { + protection: from_candid_AuthnMethodProtection_n186(value.protection), + purpose: from_candid_AuthnMethodPurpose_n188(value.purpose) + }; +} +function from_candid_record_n192(value: { + expiration: _Timestamp; + session: [] | [Principal]; + authn_method: [] | [_AuthnMethodData]; +}): { + expiration: Timestamp; + session?: Principal; + authn_method?: AuthnMethodData; +} { + return { + expiration: value.expiration, + session: record_opt_to_undefined(from_candid_opt_n73(value.session)), + authn_method: record_opt_to_undefined(from_candid_opt_n193(value.authn_method)) + }; +} +function from_candid_record_n239(value: { + storage_layout_version: number; + users_registered: bigint; + assigned_user_number_range: [bigint, bigint]; + archive_info: _ArchiveInfo; + canister_creation_cycles_cost: bigint; + event_aggregations: Array<[string, Array<[string, bigint]>]>; +}): { + storage_layout_version: number; + users_registered: bigint; + assigned_user_number_range: [bigint, bigint]; + archive_info: ArchiveInfo; + canister_creation_cycles_cost: bigint; + event_aggregations: Array<[string, Array<[string, bigint]>]>; +} { + return { + storage_layout_version: value.storage_layout_version, + users_registered: value.users_registered, + assigned_user_number_range: value.assigned_user_number_range, + archive_info: from_candid_ArchiveInfo_n240(value.archive_info), + canister_creation_cycles_cost: value.canister_creation_cycles_cost, + event_aggregations: value.event_aggregations + }; +} +function from_candid_record_n241(value: { + archive_config: [] | [_ArchiveConfig]; + archive_canister: [] | [Principal]; +}): { + archive_config?: ArchiveConfig; + archive_canister?: Principal; +} { + return { + archive_config: record_opt_to_undefined(from_candid_opt_n76(value.archive_config)), + archive_canister: record_opt_to_undefined(from_candid_opt_n73(value.archive_canister)) + }; +} +function from_candid_record_n60(value: { + name: [] | [string]; + created_at: [] | [_Timestamp]; +}): { + name?: string; + created_at?: Timestamp; +} { + return { + name: record_opt_to_undefined(from_candid_opt_n61(value.name)), + created_at: record_opt_to_undefined(from_candid_opt_n62(value.created_at)) + }; +} +function from_candid_record_n65(value: { + next_step: _RegistrationFlowNextStep; +}): { + next_step: RegistrationFlowNextStep; +} { + return { + next_step: from_candid_RegistrationFlowNextStep_n66(value.next_step) + }; +} +function from_candid_record_n71(value: { + fetch_root_key: [] | [boolean]; + is_production: [] | [boolean]; + backend_canister_id: [] | [Principal]; + enable_dapps_explorer: [] | [boolean]; + assigned_user_number_range: [] | [[bigint, bigint]]; + new_flow_origins: [] | [Array]; + archive_config: [] | [_ArchiveConfig]; + canister_creation_cycles_cost: [] | [bigint]; + analytics_config: [] | [[] | [_AnalyticsConfig]]; + related_origins: [] | [Array]; + openid_configs: [] | [Array<_OpenIdConfig>]; + backend_origin: [] | [string]; + captcha_config: [] | [_CaptchaConfig]; + dummy_auth: [] | [[] | [_DummyAuthConfig]]; + register_rate_limit: [] | [_RateLimitConfig]; +}): { + fetch_root_key?: boolean; + is_production?: boolean; + backend_canister_id?: Principal; + enable_dapps_explorer?: boolean; + assigned_user_number_range?: [bigint, bigint]; + new_flow_origins?: Array; + archive_config?: ArchiveConfig; + canister_creation_cycles_cost?: bigint; + analytics_config?: AnalyticsConfig | null; + related_origins?: Array; + openid_configs?: Array; + backend_origin?: string; + captcha_config?: CaptchaConfig; + dummy_auth?: DummyAuthConfig | null; + register_rate_limit?: RateLimitConfig; +} { + return { + fetch_root_key: record_opt_to_undefined(from_candid_opt_n72(value.fetch_root_key)), + is_production: record_opt_to_undefined(from_candid_opt_n72(value.is_production)), + backend_canister_id: record_opt_to_undefined(from_candid_opt_n73(value.backend_canister_id)), + enable_dapps_explorer: record_opt_to_undefined(from_candid_opt_n72(value.enable_dapps_explorer)), + assigned_user_number_range: record_opt_to_undefined(from_candid_opt_n74(value.assigned_user_number_range)), + new_flow_origins: record_opt_to_undefined(from_candid_opt_n75(value.new_flow_origins)), + archive_config: record_opt_to_undefined(from_candid_opt_n76(value.archive_config)), + canister_creation_cycles_cost: record_opt_to_undefined(from_candid_opt_n77(value.canister_creation_cycles_cost)), + analytics_config: record_opt_to_undefined(from_candid_opt_n78(value.analytics_config)), + related_origins: record_opt_to_undefined(from_candid_opt_n75(value.related_origins)), + openid_configs: record_opt_to_undefined(from_candid_opt_n83(value.openid_configs)), + backend_origin: record_opt_to_undefined(from_candid_opt_n61(value.backend_origin)), + captcha_config: record_opt_to_undefined(from_candid_opt_n90(value.captcha_config)), + dummy_auth: record_opt_to_undefined(from_candid_opt_n95(value.dummy_auth)), + register_rate_limit: record_opt_to_undefined(from_candid_opt_n97(value.register_rate_limit)) + }; +} +function from_candid_record_n82(value: { + domain: [] | [string]; + track_localhost: [] | [boolean]; + hash_mode: [] | [boolean]; + api_host: [] | [string]; +}): { + domain?: string; + track_localhost?: boolean; + hash_mode?: boolean; + api_host?: string; +} { + return { + domain: record_opt_to_undefined(from_candid_opt_n61(value.domain)), + track_localhost: record_opt_to_undefined(from_candid_opt_n72(value.track_localhost)), + hash_mode: record_opt_to_undefined(from_candid_opt_n72(value.hash_mode)), + api_host: record_opt_to_undefined(from_candid_opt_n61(value.api_host)) + }; +} +function from_candid_record_n86(value: { + auth_uri: string; + jwks_uri: string; + logo: string; + name: string; + fedcm_uri: [] | [string]; + email_verification: [] | [_OpenIdEmailVerification]; + issuer: string; + auth_scope: Array; + client_id: string; +}): { + auth_uri: string; + jwks_uri: string; + logo: string; + name: string; + fedcm_uri?: string; + email_verification?: OpenIdEmailVerification; + issuer: string; + auth_scope: Array; + client_id: string; +} { + return { + auth_uri: value.auth_uri, + jwks_uri: value.jwks_uri, + logo: value.logo, + name: value.name, + fedcm_uri: record_opt_to_undefined(from_candid_opt_n61(value.fedcm_uri)), + email_verification: record_opt_to_undefined(from_candid_opt_n87(value.email_verification)), + issuer: value.issuer, + auth_scope: value.auth_scope, + client_id: value.client_id + }; +} +function from_candid_record_n92(value: { + max_unsolved_captchas: bigint; + captcha_trigger: { + Dynamic: { + reference_rate_sampling_interval_s: bigint; + threshold_pct: number; + current_rate_sampling_interval_s: bigint; + }; + } | { + Static: { + CaptchaDisabled: null; + } | { + CaptchaEnabled: null; + }; + }; +}): { + max_unsolved_captchas: bigint; + captcha_trigger: { + __kind__: "Dynamic"; + Dynamic: { + reference_rate_sampling_interval_s: bigint; + threshold_pct: number; + current_rate_sampling_interval_s: bigint; + }; + } | { + __kind__: "Static"; + Static: Variant_CaptchaDisabled_CaptchaEnabled; + }; +} { + return { + max_unsolved_captchas: value.max_unsolved_captchas, + captcha_trigger: from_candid_variant_n93(value.captcha_trigger) + }; +} +function from_candid_tuple_n127(value: [string, { + map: _MetadataMap; + } | { + string: string; + } | { + bytes: Uint8Array; + }]): [string, { + __kind__: "map"; + map: MetadataMap; + } | { + __kind__: "string"; + string: string; + } | { + __kind__: "bytes"; + bytes: Uint8Array; + }] { + return [ + value[0], + from_candid_variant_n128(value[1]) + ]; +} +function from_candid_tuple_n143(value: [string, { + Map: _MetadataMapV2; + } | { + String: string; + } | { + Bytes: Uint8Array; + }]): [string, { + __kind__: "Map"; + Map: MetadataMapV2; + } | { + __kind__: "String"; + String: string; + } | { + __kind__: "Bytes"; + Bytes: Uint8Array; + }] { + return [ + value[0], + from_candid_variant_n144(value[1]) + ]; +} +function from_candid_variant_n103(value: { + AccountLimitReached: null; +} | { + InternalCanisterError: string; +} | { + Unauthorized: Principal; +} | { + NameTooLong: null; +}): { + __kind__: "AccountLimitReached"; + AccountLimitReached: null; +} | { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +} | { + __kind__: "NameTooLong"; + NameTooLong: null; +} { + return "AccountLimitReached" in value ? { + __kind__: "AccountLimitReached", + AccountLimitReached: value.AccountLimitReached + } : "InternalCanisterError" in value ? { + __kind__: "InternalCanisterError", + InternalCanisterError: value.InternalCanisterError + } : "Unauthorized" in value ? { + __kind__: "Unauthorized", + Unauthorized: value.Unauthorized + } : "NameTooLong" in value ? { + __kind__: "NameTooLong", + NameTooLong: value.NameTooLong + } : value; +} +function from_candid_variant_n105(value: { + creation_in_progress: null; +} | { + success: Principal; +} | { + failed: string; +}): { + __kind__: "creation_in_progress"; + creation_in_progress: null; +} | { + __kind__: "success"; + success: Principal; +} | { + __kind__: "failed"; + failed: string; +} { + return "creation_in_progress" in value ? { + __kind__: "creation_in_progress", + creation_in_progress: value.creation_in_progress + } : "success" in value ? { + __kind__: "success", + success: value.success + } : "failed" in value ? { + __kind__: "failed", + failed: value.failed + } : value; +} +function from_candid_variant_n107(value: { + Ok: _SignedDelegation; +} | { + Err: _AccountDelegationError; +}): { + __kind__: "Ok"; + Ok: SignedDelegation; +} | { + __kind__: "Err"; + Err: AccountDelegationError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: from_candid_SignedDelegation_n108(value.Ok) + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_AccountDelegationError_n113(value.Err) + } : value; +} +function from_candid_variant_n114(value: { + NoSuchDelegation: null; +} | { + InternalCanisterError: string; +} | { + Unauthorized: Principal; +}): { + __kind__: "NoSuchDelegation"; + NoSuchDelegation: null; +} | { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +} { + return "NoSuchDelegation" in value ? { + __kind__: "NoSuchDelegation", + NoSuchDelegation: value.NoSuchDelegation + } : "InternalCanisterError" in value ? { + __kind__: "InternalCanisterError", + InternalCanisterError: value.InternalCanisterError + } : "Unauthorized" in value ? { + __kind__: "Unauthorized", + Unauthorized: value.Unauthorized + } : value; +} +function from_candid_variant_n115(value: { + Ok: Array<_AccountInfo>; +} | { + Err: _GetAccountsError; +}): { + __kind__: "Ok"; + Ok: Array; +} | { + __kind__: "Err"; + Err: GetAccountsError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: from_candid_vec_n116(value.Ok) + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_GetAccountsError_n117(value.Err) + } : value; +} +function from_candid_variant_n118(value: { + InternalCanisterError: string; +} | { + Unauthorized: Principal; +}): { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +} { + return "InternalCanisterError" in value ? { + __kind__: "InternalCanisterError", + InternalCanisterError: value.InternalCanisterError + } : "Unauthorized" in value ? { + __kind__: "Unauthorized", + Unauthorized: value.Unauthorized + } : value; +} +function from_candid_variant_n128(value: { + map: _MetadataMap; +} | { + string: string; +} | { + bytes: Uint8Array; +}): { + __kind__: "map"; + map: MetadataMap; +} | { + __kind__: "string"; + string: string; +} | { + __kind__: "bytes"; + bytes: Uint8Array; +} { + return "map" in value ? { + __kind__: "map", + map: from_candid_MetadataMap_n125(value.map) + } : "string" in value ? { + __kind__: "string", + string: value.string + } : "bytes" in value ? { + __kind__: "bytes", + bytes: value.bytes + } : value; +} +function from_candid_variant_n130(value: { + unprotected: null; +} | { + protected: null; +}): DeviceProtection { + return "unprotected" in value ? DeviceProtection.unprotected : "protected" in value ? DeviceProtection.protected : value; +} +function from_candid_variant_n132(value: { + platform: null; +} | { + seed_phrase: null; +} | { + cross_platform: null; +} | { + unknown: null; +} | { + browser_storage_key: null; +}): KeyType { + return "platform" in value ? KeyType.platform : "seed_phrase" in value ? KeyType.seed_phrase : "cross_platform" in value ? KeyType.cross_platform : "unknown" in value ? KeyType.unknown : "browser_storage_key" in value ? KeyType.browser_storage_key : value; +} +function from_candid_variant_n135(value: { + authentication: null; +} | { + recovery: null; +}): Purpose { + return "authentication" in value ? Purpose.authentication : "recovery" in value ? Purpose.recovery : value; +} +function from_candid_variant_n14(value: { + device_registration_mode_off: null; +} | { + another_device_tentatively_added: null; +} | { + passkey_with_this_public_key_is_already_used: null; +} | { + added_tentatively: { + verification_code: string; + device_registration_timeout: _Timestamp; + }; +}): { + __kind__: "device_registration_mode_off"; + device_registration_mode_off: null; +} | { + __kind__: "another_device_tentatively_added"; + another_device_tentatively_added: null; +} | { + __kind__: "passkey_with_this_public_key_is_already_used"; + passkey_with_this_public_key_is_already_used: null; +} | { + __kind__: "added_tentatively"; + added_tentatively: { + verification_code: string; + device_registration_timeout: Timestamp; + }; +} { + return "device_registration_mode_off" in value ? { + __kind__: "device_registration_mode_off", + device_registration_mode_off: value.device_registration_mode_off + } : "another_device_tentatively_added" in value ? { + __kind__: "another_device_tentatively_added", + another_device_tentatively_added: value.another_device_tentatively_added + } : "passkey_with_this_public_key_is_already_used" in value ? { + __kind__: "passkey_with_this_public_key_is_already_used", + passkey_with_this_public_key_is_already_used: value.passkey_with_this_public_key_is_already_used + } : "added_tentatively" in value ? { + __kind__: "added_tentatively", + added_tentatively: value.added_tentatively + } : value; +} +function from_candid_variant_n144(value: { + Map: _MetadataMapV2; +} | { + String: string; +} | { + Bytes: Uint8Array; +}): { + __kind__: "Map"; + Map: MetadataMapV2; +} | { + __kind__: "String"; + String: string; +} | { + __kind__: "Bytes"; + Bytes: Uint8Array; +} { + return "Map" in value ? { + __kind__: "Map", + Map: from_candid_MetadataMapV2_n141(value.Map) + } : "String" in value ? { + __kind__: "String", + String: value.String + } : "Bytes" in value ? { + __kind__: "Bytes", + Bytes: value.Bytes + } : value; +} +function from_candid_variant_n153(value: { + Ok: _CertifiedAttributes; +} | { + Err: _GetAttributesError; +}): { + __kind__: "Ok"; + Ok: CertifiedAttributes; +} | { + __kind__: "Err"; + Err: GetAttributesError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: value.Ok + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_GetAttributesError_n154(value.Err) + } : value; +} +function from_candid_variant_n155(value: { + AuthorizationError: Principal; +} | { + ValidationError: { + problems: Array; + }; +} | { + GetAccountError: _GetAccountError; +}): { + __kind__: "AuthorizationError"; + AuthorizationError: Principal; +} | { + __kind__: "ValidationError"; + ValidationError: { + problems: Array; + }; +} | { + __kind__: "GetAccountError"; + GetAccountError: GetAccountError; +} { + return "AuthorizationError" in value ? { + __kind__: "AuthorizationError", + AuthorizationError: value.AuthorizationError + } : "ValidationError" in value ? { + __kind__: "ValidationError", + ValidationError: value.ValidationError + } : "GetAccountError" in value ? { + __kind__: "GetAccountError", + GetAccountError: from_candid_GetAccountError_n156(value.GetAccountError) + } : value; +} +function from_candid_variant_n157(value: { + NoSuchOrigin: { + anchor_number: _UserNumber; + }; +} | { + NoSuchAccount: { + origin: _FrontendHostname; + anchor_number: _UserNumber; + }; +}): { + __kind__: "NoSuchOrigin"; + NoSuchOrigin: { + anchor_number: UserNumber; + }; +} | { + __kind__: "NoSuchAccount"; + NoSuchAccount: { + origin: FrontendHostname; + anchor_number: UserNumber; + }; +} { + return "NoSuchOrigin" in value ? { + __kind__: "NoSuchOrigin", + NoSuchOrigin: value.NoSuchOrigin + } : "NoSuchAccount" in value ? { + __kind__: "NoSuchAccount", + NoSuchAccount: value.NoSuchAccount + } : value; +} +function from_candid_variant_n158(value: { + Ok: _AccountInfo; +} | { + Err: _GetDefaultAccountError; +}): { + __kind__: "Ok"; + Ok: AccountInfo; +} | { + __kind__: "Err"; + Err: GetDefaultAccountError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: from_candid_AccountInfo_n99(value.Ok) + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_GetDefaultAccountError_n159(value.Err) + } : value; +} +function from_candid_variant_n160(value: { + NoSuchOrigin: { + anchor_number: _UserNumber; + }; +} | { + NoSuchAnchor: null; +} | { + InternalCanisterError: string; +} | { + Unauthorized: Principal; +}): { + __kind__: "NoSuchOrigin"; + NoSuchOrigin: { + anchor_number: UserNumber; + }; +} | { + __kind__: "NoSuchAnchor"; + NoSuchAnchor: null; +} | { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +} { + return "NoSuchOrigin" in value ? { + __kind__: "NoSuchOrigin", + NoSuchOrigin: value.NoSuchOrigin + } : "NoSuchAnchor" in value ? { + __kind__: "NoSuchAnchor", + NoSuchAnchor: value.NoSuchAnchor + } : "InternalCanisterError" in value ? { + __kind__: "InternalCanisterError", + InternalCanisterError: value.InternalCanisterError + } : "Unauthorized" in value ? { + __kind__: "Unauthorized", + Unauthorized: value.Unauthorized + } : value; +} +function from_candid_variant_n162(value: { + no_such_delegation: null; +} | { + signed_delegation: _SignedDelegation; +}): { + __kind__: "no_such_delegation"; + no_such_delegation: null; +} | { + __kind__: "signed_delegation"; + signed_delegation: SignedDelegation; +} { + return "no_such_delegation" in value ? { + __kind__: "no_such_delegation", + no_such_delegation: value.no_such_delegation + } : "signed_delegation" in value ? { + __kind__: "signed_delegation", + signed_delegation: from_candid_SignedDelegation_n108(value.signed_delegation) + } : value; +} +function from_candid_variant_n163(value: { + Ok: _IdAliasCredentials; +} | { + Err: _GetIdAliasError; +}): { + __kind__: "Ok"; + Ok: IdAliasCredentials; +} | { + __kind__: "Err"; + Err: GetIdAliasError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: value.Ok + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_GetIdAliasError_n164(value.Err) + } : value; +} +function from_candid_variant_n165(value: { + InternalCanisterError: string; +} | { + Unauthorized: Principal; +} | { + NoSuchCredentials: string; +}): { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +} | { + __kind__: "NoSuchCredentials"; + NoSuchCredentials: string; +} { + return "InternalCanisterError" in value ? { + __kind__: "InternalCanisterError", + InternalCanisterError: value.InternalCanisterError + } : "Unauthorized" in value ? { + __kind__: "Unauthorized", + Unauthorized: value.Unauthorized + } : "NoSuchCredentials" in value ? { + __kind__: "NoSuchCredentials", + NoSuchCredentials: value.NoSuchCredentials + } : value; +} +function from_candid_variant_n170(value: { + Ok: _IdentityAuthnInfo; +} | { + Err: null; +}): { + __kind__: "Ok"; + Ok: IdentityAuthnInfo; +} | { + __kind__: "Err"; + Err: null; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: from_candid_IdentityAuthnInfo_n171(value.Ok) + } : "Err" in value ? { + __kind__: "Err", + Err: value.Err + } : value; +} +function from_candid_variant_n175(value: { + PubKey: _PublicKeyAuthn; +} | { + WebAuthn: _WebAuthn; +}): { + __kind__: "PubKey"; + PubKey: PublicKeyAuthn; +} | { + __kind__: "WebAuthn"; + WebAuthn: WebAuthn; +} { + return "PubKey" in value ? { + __kind__: "PubKey", + PubKey: value.PubKey + } : "WebAuthn" in value ? { + __kind__: "WebAuthn", + WebAuthn: from_candid_WebAuthn_n176(value.WebAuthn) + } : value; +} +function from_candid_variant_n178(value: { + Ok: _IdentityInfo; +} | { + Err: _IdentityInfoError; +}): { + __kind__: "Ok"; + Ok: IdentityInfo; +} | { + __kind__: "Err"; + Err: IdentityInfoError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: from_candid_IdentityInfo_n179(value.Ok) + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_IdentityInfoError_n194(value.Err) + } : value; +} +function from_candid_variant_n187(value: { + Protected: null; +} | { + Unprotected: null; +}): AuthnMethodProtection { + return "Protected" in value ? AuthnMethodProtection.Protected : "Unprotected" in value ? AuthnMethodProtection.Unprotected : value; +} +function from_candid_variant_n189(value: { + Recovery: null; +} | { + Authentication: null; +}): AuthnMethodPurpose { + return "Recovery" in value ? AuthnMethodPurpose.Recovery : "Authentication" in value ? AuthnMethodPurpose.Authentication : value; +} +function from_candid_variant_n195(value: { + Ok: null; +} | { + Err: _IdentityMetadataReplaceError; +}): { + __kind__: "Ok"; + Ok: null; +} | { + __kind__: "Err"; + Err: IdentityMetadataReplaceError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: value.Ok + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_IdentityMetadataReplaceError_n196(value.Err) + } : value; +} +function from_candid_variant_n197(value: { + InternalCanisterError: string; +} | { + Unauthorized: Principal; +} | { + StorageSpaceExceeded: { + space_required: bigint; + space_available: bigint; + }; +}): { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +} | { + __kind__: "StorageSpaceExceeded"; + StorageSpaceExceeded: { + space_required: bigint; + space_available: bigint; + }; +} { + return "InternalCanisterError" in value ? { + __kind__: "InternalCanisterError", + InternalCanisterError: value.InternalCanisterError + } : "Unauthorized" in value ? { + __kind__: "Unauthorized", + Unauthorized: value.Unauthorized + } : "StorageSpaceExceeded" in value ? { + __kind__: "StorageSpaceExceeded", + StorageSpaceExceeded: value.StorageSpaceExceeded + } : value; +} +function from_candid_variant_n200(value: { + Ok: null; +} | { + Err: _IdentityPropertiesReplaceError; +}): { + __kind__: "Ok"; + Ok: null; +} | { + __kind__: "Err"; + Err: IdentityPropertiesReplaceError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: value.Ok + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_IdentityPropertiesReplaceError_n201(value.Err) + } : value; +} +function from_candid_variant_n202(value: { + InternalCanisterError: string; +} | { + Unauthorized: Principal; +} | { + NameTooLong: { + limit: bigint; + }; +} | { + StorageSpaceExceeded: { + space_required: bigint; + space_available: bigint; + }; +}): { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +} | { + __kind__: "NameTooLong"; + NameTooLong: { + limit: bigint; + }; +} | { + __kind__: "StorageSpaceExceeded"; + StorageSpaceExceeded: { + space_required: bigint; + space_available: bigint; + }; +} { + return "InternalCanisterError" in value ? { + __kind__: "InternalCanisterError", + InternalCanisterError: value.InternalCanisterError + } : "Unauthorized" in value ? { + __kind__: "Unauthorized", + Unauthorized: value.Unauthorized + } : "NameTooLong" in value ? { + __kind__: "NameTooLong", + NameTooLong: value.NameTooLong + } : "StorageSpaceExceeded" in value ? { + __kind__: "StorageSpaceExceeded", + StorageSpaceExceeded: value.StorageSpaceExceeded + } : value; +} +function from_candid_variant_n205(value: { + Ok: _IdRegFinishResult; +} | { + Err: _IdRegFinishError; +}): { + __kind__: "Ok"; + Ok: IdRegFinishResult; +} | { + __kind__: "Err"; + Err: IdRegFinishError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: value.Ok + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_IdRegFinishError_n206(value.Err) + } : value; +} +function from_candid_variant_n207(value: { + NoRegistrationFlow: null; +} | { + UnexpectedCall: { + next_step: _RegistrationFlowNextStep; + }; +} | { + InvalidAuthnMethod: string; +} | { + StorageError: string; +}): { + __kind__: "NoRegistrationFlow"; + NoRegistrationFlow: null; +} | { + __kind__: "UnexpectedCall"; + UnexpectedCall: { + next_step: RegistrationFlowNextStep; + }; +} | { + __kind__: "InvalidAuthnMethod"; + InvalidAuthnMethod: string; +} | { + __kind__: "StorageError"; + StorageError: string; +} { + return "NoRegistrationFlow" in value ? { + __kind__: "NoRegistrationFlow", + NoRegistrationFlow: value.NoRegistrationFlow + } : "UnexpectedCall" in value ? { + __kind__: "UnexpectedCall", + UnexpectedCall: from_candid_record_n65(value.UnexpectedCall) + } : "InvalidAuthnMethod" in value ? { + __kind__: "InvalidAuthnMethod", + InvalidAuthnMethod: value.InvalidAuthnMethod + } : "StorageError" in value ? { + __kind__: "StorageError", + StorageError: value.StorageError + } : value; +} +function from_candid_variant_n208(value: { + Ok: _IdRegNextStepResult; +} | { + Err: _IdRegStartError; +}): { + __kind__: "Ok"; + Ok: IdRegNextStepResult; +} | { + __kind__: "Err"; + Err: IdRegStartError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: from_candid_IdRegNextStepResult_n64(value.Ok) + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_IdRegStartError_n209(value.Err) + } : value; +} +function from_candid_variant_n210(value: { + InvalidCaller: null; +} | { + AlreadyInProgress: null; +} | { + RateLimitExceeded: null; +}): IdRegStartError { + return "InvalidCaller" in value ? IdRegStartError.InvalidCaller : "AlreadyInProgress" in value ? IdRegStartError.AlreadyInProgress : "RateLimitExceeded" in value ? IdRegStartError.RateLimitExceeded : value; +} +function from_candid_variant_n214(value: { + Ok: null; +} | { + Err: _OpenIdCredentialAddError; +}): { + __kind__: "Ok"; + Ok: null; +} | { + __kind__: "Err"; + Err: OpenIdCredentialAddError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: value.Ok + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_OpenIdCredentialAddError_n215(value.Err) + } : value; +} +function from_candid_variant_n216(value: { + OpenIdCredentialAlreadyRegistered: null; +} | { + InternalCanisterError: string; +} | { + JwtExpired: null; +} | { + Unauthorized: Principal; +} | { + JwtVerificationFailed: null; +}): { + __kind__: "OpenIdCredentialAlreadyRegistered"; + OpenIdCredentialAlreadyRegistered: null; +} | { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "JwtExpired"; + JwtExpired: null; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +} | { + __kind__: "JwtVerificationFailed"; + JwtVerificationFailed: null; +} { + return "OpenIdCredentialAlreadyRegistered" in value ? { + __kind__: "OpenIdCredentialAlreadyRegistered", + OpenIdCredentialAlreadyRegistered: value.OpenIdCredentialAlreadyRegistered + } : "InternalCanisterError" in value ? { + __kind__: "InternalCanisterError", + InternalCanisterError: value.InternalCanisterError + } : "JwtExpired" in value ? { + __kind__: "JwtExpired", + JwtExpired: value.JwtExpired + } : "Unauthorized" in value ? { + __kind__: "Unauthorized", + Unauthorized: value.Unauthorized + } : "JwtVerificationFailed" in value ? { + __kind__: "JwtVerificationFailed", + JwtVerificationFailed: value.JwtVerificationFailed + } : value; +} +function from_candid_variant_n217(value: { + Ok: null; +} | { + Err: _OpenIdCredentialRemoveError; +}): { + __kind__: "Ok"; + Ok: null; +} | { + __kind__: "Err"; + Err: OpenIdCredentialRemoveError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: value.Ok + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_OpenIdCredentialRemoveError_n218(value.Err) + } : value; +} +function from_candid_variant_n219(value: { + InternalCanisterError: string; +} | { + OpenIdCredentialNotFound: null; +} | { + Unauthorized: Principal; +}): { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "OpenIdCredentialNotFound"; + OpenIdCredentialNotFound: null; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +} { + return "InternalCanisterError" in value ? { + __kind__: "InternalCanisterError", + InternalCanisterError: value.InternalCanisterError + } : "OpenIdCredentialNotFound" in value ? { + __kind__: "OpenIdCredentialNotFound", + OpenIdCredentialNotFound: value.OpenIdCredentialNotFound + } : "Unauthorized" in value ? { + __kind__: "Unauthorized", + Unauthorized: value.Unauthorized + } : value; +} +function from_candid_variant_n220(value: { + Ok: _SignedDelegation; +} | { + Err: _OpenIdDelegationError; +}): { + __kind__: "Ok"; + Ok: SignedDelegation; +} | { + __kind__: "Err"; + Err: OpenIdDelegationError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: from_candid_SignedDelegation_n108(value.Ok) + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_OpenIdDelegationError_n221(value.Err) + } : value; +} +function from_candid_variant_n222(value: { + NoSuchDelegation: null; +} | { + NoSuchAnchor: null; +} | { + JwtExpired: null; +} | { + JwtVerificationFailed: null; +}): OpenIdDelegationError { + return "NoSuchDelegation" in value ? OpenIdDelegationError.NoSuchDelegation : "NoSuchAnchor" in value ? OpenIdDelegationError.NoSuchAnchor : "JwtExpired" in value ? OpenIdDelegationError.JwtExpired : "JwtVerificationFailed" in value ? OpenIdDelegationError.JwtVerificationFailed : value; +} +function from_candid_variant_n223(value: { + Ok: _OpenIdPrepareDelegationResponse; +} | { + Err: _OpenIdDelegationError; +}): { + __kind__: "Ok"; + Ok: OpenIdPrepareDelegationResponse; +} | { + __kind__: "Err"; + Err: OpenIdDelegationError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: value.Ok + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_OpenIdDelegationError_n221(value.Err) + } : value; +} +function from_candid_variant_n225(value: { + Ok: _PrepareAccountDelegation; +} | { + Err: _AccountDelegationError; +}): { + __kind__: "Ok"; + Ok: PrepareAccountDelegation; +} | { + __kind__: "Err"; + Err: AccountDelegationError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: value.Ok + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_AccountDelegationError_n113(value.Err) + } : value; +} +function from_candid_variant_n228(value: { + Ok: _PrepareAttributeResponse; +} | { + Err: _PrepareAttributeError; +}): { + __kind__: "Ok"; + Ok: PrepareAttributeResponse; +} | { + __kind__: "Err"; + Err: PrepareAttributeError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: value.Ok + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_PrepareAttributeError_n229(value.Err) + } : value; +} +function from_candid_variant_n230(value: { + Ok: _PreparedIdAlias; +} | { + Err: _PrepareIdAliasError; +}): { + __kind__: "Ok"; + Ok: PreparedIdAlias; +} | { + __kind__: "Err"; + Err: PrepareIdAliasError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: value.Ok + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_PrepareIdAliasError_n231(value.Err) + } : value; +} +function from_candid_variant_n234(value: { + bad_challenge: null; +} | { + canister_full: null; +} | { + registered: { + user_number: _UserNumber; + }; +}): { + __kind__: "bad_challenge"; + bad_challenge: null; +} | { + __kind__: "canister_full"; + canister_full: null; +} | { + __kind__: "registered"; + registered: { + user_number: UserNumber; + }; +} { + return "bad_challenge" in value ? { + __kind__: "bad_challenge", + bad_challenge: value.bad_challenge + } : "canister_full" in value ? { + __kind__: "canister_full", + canister_full: value.canister_full + } : "registered" in value ? { + __kind__: "registered", + registered: value.registered + } : value; +} +function from_candid_variant_n235(value: { + Ok: _AccountInfo; +} | { + Err: _SetDefaultAccountError; +}): { + __kind__: "Ok"; + Ok: AccountInfo; +} | { + __kind__: "Err"; + Err: SetDefaultAccountError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: from_candid_AccountInfo_n99(value.Ok) + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_SetDefaultAccountError_n236(value.Err) + } : value; +} +function from_candid_variant_n237(value: { + NoSuchOrigin: { + anchor_number: _UserNumber; + }; +} | { + NoSuchAnchor: null; +} | { + InternalCanisterError: string; +} | { + Unauthorized: Principal; +} | { + NoSuchAccount: { + origin: _FrontendHostname; + anchor_number: _UserNumber; + }; +}): { + __kind__: "NoSuchOrigin"; + NoSuchOrigin: { + anchor_number: UserNumber; + }; +} | { + __kind__: "NoSuchAnchor"; + NoSuchAnchor: null; +} | { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +} | { + __kind__: "NoSuchAccount"; + NoSuchAccount: { + origin: FrontendHostname; + anchor_number: UserNumber; + }; +} { + return "NoSuchOrigin" in value ? { + __kind__: "NoSuchOrigin", + NoSuchOrigin: value.NoSuchOrigin + } : "NoSuchAnchor" in value ? { + __kind__: "NoSuchAnchor", + NoSuchAnchor: value.NoSuchAnchor + } : "InternalCanisterError" in value ? { + __kind__: "InternalCanisterError", + InternalCanisterError: value.InternalCanisterError + } : "Unauthorized" in value ? { + __kind__: "Unauthorized", + Unauthorized: value.Unauthorized + } : "NoSuchAccount" in value ? { + __kind__: "NoSuchAccount", + NoSuchAccount: value.NoSuchAccount + } : value; +} +function from_candid_variant_n243(value: { + Ok: _AccountInfo; +} | { + Err: _UpdateAccountError; +}): { + __kind__: "Ok"; + Ok: AccountInfo; +} | { + __kind__: "Err"; + Err: UpdateAccountError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: from_candid_AccountInfo_n99(value.Ok) + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_UpdateAccountError_n244(value.Err) + } : value; +} +function from_candid_variant_n246(value: { + device_registration_mode_off: null; +} | { + verified: null; +} | { + wrong_code: { + retries_left: number; + }; +} | { + no_device_to_verify: null; +}): { + __kind__: "device_registration_mode_off"; + device_registration_mode_off: null; +} | { + __kind__: "verified"; + verified: null; +} | { + __kind__: "wrong_code"; + wrong_code: { + retries_left: number; + }; +} | { + __kind__: "no_device_to_verify"; + no_device_to_verify: null; +} { + return "device_registration_mode_off" in value ? { + __kind__: "device_registration_mode_off", + device_registration_mode_off: value.device_registration_mode_off + } : "verified" in value ? { + __kind__: "verified", + verified: value.verified + } : "wrong_code" in value ? { + __kind__: "wrong_code", + wrong_code: value.wrong_code + } : "no_device_to_verify" in value ? { + __kind__: "no_device_to_verify", + no_device_to_verify: value.no_device_to_verify + } : value; +} +function from_candid_variant_n31(value: { + Ok: null; +} | { + Err: _AuthnMethodAddError; +}): { + __kind__: "Ok"; + Ok: null; +} | { + __kind__: "Err"; + Err: AuthnMethodAddError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: value.Ok + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_AuthnMethodAddError_n32(value.Err) + } : value; +} +function from_candid_variant_n33(value: { + InvalidMetadata: string; +}): { + __kind__: "InvalidMetadata"; + InvalidMetadata: string; +} { + return "InvalidMetadata" in value ? { + __kind__: "InvalidMetadata", + InvalidMetadata: value.InvalidMetadata + } : value; +} +function from_candid_variant_n34(value: { + Ok: null; +} | { + Err: _AuthnMethodConfirmationError; +}): { + __kind__: "Ok"; + Ok: null; +} | { + __kind__: "Err"; + Err: AuthnMethodConfirmationError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: value.Ok + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_AuthnMethodConfirmationError_n35(value.Err) + } : value; +} +function from_candid_variant_n36(value: { + InternalCanisterError: string; +} | { + RegistrationModeOff: null; +} | { + Unauthorized: Principal; +} | { + NoAuthnMethodToConfirm: null; +} | { + WrongCode: { + retries_left: number; + }; +}): { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "RegistrationModeOff"; + RegistrationModeOff: null; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +} | { + __kind__: "NoAuthnMethodToConfirm"; + NoAuthnMethodToConfirm: null; +} | { + __kind__: "WrongCode"; + WrongCode: { + retries_left: number; + }; +} { + return "InternalCanisterError" in value ? { + __kind__: "InternalCanisterError", + InternalCanisterError: value.InternalCanisterError + } : "RegistrationModeOff" in value ? { + __kind__: "RegistrationModeOff", + RegistrationModeOff: value.RegistrationModeOff + } : "Unauthorized" in value ? { + __kind__: "Unauthorized", + Unauthorized: value.Unauthorized + } : "NoAuthnMethodToConfirm" in value ? { + __kind__: "NoAuthnMethodToConfirm", + NoAuthnMethodToConfirm: value.NoAuthnMethodToConfirm + } : "WrongCode" in value ? { + __kind__: "WrongCode", + WrongCode: value.WrongCode + } : value; +} +function from_candid_variant_n37(value: { + Ok: null; +} | { + Err: _AuthnMethodMetadataReplaceError; +}): { + __kind__: "Ok"; + Ok: null; +} | { + __kind__: "Err"; + Err: AuthnMethodMetadataReplaceError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: value.Ok + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_AuthnMethodMetadataReplaceError_n38(value.Err) + } : value; +} +function from_candid_variant_n39(value: { + AuthnMethodNotFound: null; +} | { + InvalidMetadata: string; +}): { + __kind__: "AuthnMethodNotFound"; + AuthnMethodNotFound: null; +} | { + __kind__: "InvalidMetadata"; + InvalidMetadata: string; +} { + return "AuthnMethodNotFound" in value ? { + __kind__: "AuthnMethodNotFound", + AuthnMethodNotFound: value.AuthnMethodNotFound + } : "InvalidMetadata" in value ? { + __kind__: "InvalidMetadata", + InvalidMetadata: value.InvalidMetadata + } : value; +} +function from_candid_variant_n40(value: { + Ok: _AuthnMethodConfirmationCode; +} | { + Err: _AuthnMethodRegisterError; +}): { + __kind__: "Ok"; + Ok: AuthnMethodConfirmationCode; +} | { + __kind__: "Err"; + Err: AuthnMethodRegisterError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: value.Ok + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_AuthnMethodRegisterError_n41(value.Err) + } : value; +} +function from_candid_variant_n42(value: { + PasskeyWithThisPublicKeyIsAlreadyUsed: null; +} | { + RegistrationModeOff: null; +} | { + RegistrationAlreadyInProgress: null; +} | { + NotSelfAuthenticating: Principal; +} | { + InvalidMetadata: string; +}): { + __kind__: "PasskeyWithThisPublicKeyIsAlreadyUsed"; + PasskeyWithThisPublicKeyIsAlreadyUsed: null; +} | { + __kind__: "RegistrationModeOff"; + RegistrationModeOff: null; +} | { + __kind__: "RegistrationAlreadyInProgress"; + RegistrationAlreadyInProgress: null; +} | { + __kind__: "NotSelfAuthenticating"; + NotSelfAuthenticating: Principal; +} | { + __kind__: "InvalidMetadata"; + InvalidMetadata: string; +} { + return "PasskeyWithThisPublicKeyIsAlreadyUsed" in value ? { + __kind__: "PasskeyWithThisPublicKeyIsAlreadyUsed", + PasskeyWithThisPublicKeyIsAlreadyUsed: value.PasskeyWithThisPublicKeyIsAlreadyUsed + } : "RegistrationModeOff" in value ? { + __kind__: "RegistrationModeOff", + RegistrationModeOff: value.RegistrationModeOff + } : "RegistrationAlreadyInProgress" in value ? { + __kind__: "RegistrationAlreadyInProgress", + RegistrationAlreadyInProgress: value.RegistrationAlreadyInProgress + } : "NotSelfAuthenticating" in value ? { + __kind__: "NotSelfAuthenticating", + NotSelfAuthenticating: value.NotSelfAuthenticating + } : "InvalidMetadata" in value ? { + __kind__: "InvalidMetadata", + InvalidMetadata: value.InvalidMetadata + } : value; +} +function from_candid_variant_n44(value: { + Ok: { + expiration: _Timestamp; + }; +} | { + Err: _AuthnMethodRegistrationModeEnterError; +}): { + __kind__: "Ok"; + Ok: { + expiration: Timestamp; + }; +} | { + __kind__: "Err"; + Err: AuthnMethodRegistrationModeEnterError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: value.Ok + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_AuthnMethodRegistrationModeEnterError_n45(value.Err) + } : value; +} +function from_candid_variant_n46(value: { + InvalidRegistrationId: string; +} | { + InternalCanisterError: string; +} | { + AlreadyInProgress: null; +} | { + Unauthorized: Principal; +}): { + __kind__: "InvalidRegistrationId"; + InvalidRegistrationId: string; +} | { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "AlreadyInProgress"; + AlreadyInProgress: null; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +} { + return "InvalidRegistrationId" in value ? { + __kind__: "InvalidRegistrationId", + InvalidRegistrationId: value.InvalidRegistrationId + } : "InternalCanisterError" in value ? { + __kind__: "InternalCanisterError", + InternalCanisterError: value.InternalCanisterError + } : "AlreadyInProgress" in value ? { + __kind__: "AlreadyInProgress", + AlreadyInProgress: value.AlreadyInProgress + } : "Unauthorized" in value ? { + __kind__: "Unauthorized", + Unauthorized: value.Unauthorized + } : value; +} +function from_candid_variant_n48(value: { + Ok: null; +} | { + Err: _AuthnMethodRegistrationModeExitError; +}): { + __kind__: "Ok"; + Ok: null; +} | { + __kind__: "Err"; + Err: AuthnMethodRegistrationModeExitError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: value.Ok + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_AuthnMethodRegistrationModeExitError_n49(value.Err) + } : value; +} +function from_candid_variant_n50(value: { + PasskeyWithThisPublicKeyIsAlreadyUsed: null; +} | { + InternalCanisterError: string; +} | { + RegistrationModeOff: null; +} | { + Unauthorized: Principal; +} | { + InvalidMetadata: string; +}): { + __kind__: "PasskeyWithThisPublicKeyIsAlreadyUsed"; + PasskeyWithThisPublicKeyIsAlreadyUsed: null; +} | { + __kind__: "InternalCanisterError"; + InternalCanisterError: string; +} | { + __kind__: "RegistrationModeOff"; + RegistrationModeOff: null; +} | { + __kind__: "Unauthorized"; + Unauthorized: Principal; +} | { + __kind__: "InvalidMetadata"; + InvalidMetadata: string; +} { + return "PasskeyWithThisPublicKeyIsAlreadyUsed" in value ? { + __kind__: "PasskeyWithThisPublicKeyIsAlreadyUsed", + PasskeyWithThisPublicKeyIsAlreadyUsed: value.PasskeyWithThisPublicKeyIsAlreadyUsed + } : "InternalCanisterError" in value ? { + __kind__: "InternalCanisterError", + InternalCanisterError: value.InternalCanisterError + } : "RegistrationModeOff" in value ? { + __kind__: "RegistrationModeOff", + RegistrationModeOff: value.RegistrationModeOff + } : "Unauthorized" in value ? { + __kind__: "Unauthorized", + Unauthorized: value.Unauthorized + } : "InvalidMetadata" in value ? { + __kind__: "InvalidMetadata", + InvalidMetadata: value.InvalidMetadata + } : value; +} +function from_candid_variant_n51(value: { + Ok: null; +} | { + Err: null; +}): Variant_Ok_Err { + return "Ok" in value ? Variant_Ok_Err.Ok : "Err" in value ? Variant_Ok_Err.Err : value; +} +function from_candid_variant_n52(value: { + Ok: null; +} | { + Err: _AuthnMethodReplaceError; +}): { + __kind__: "Ok"; + Ok: null; +} | { + __kind__: "Err"; + Err: AuthnMethodReplaceError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: value.Ok + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_AuthnMethodReplaceError_n53(value.Err) + } : value; +} +function from_candid_variant_n54(value: { + PasskeyWithThisPublicKeyIsAlreadyUsed: null; +} | { + AuthnMethodNotFound: null; +} | { + InvalidMetadata: string; +}): { + __kind__: "PasskeyWithThisPublicKeyIsAlreadyUsed"; + PasskeyWithThisPublicKeyIsAlreadyUsed: null; +} | { + __kind__: "AuthnMethodNotFound"; + AuthnMethodNotFound: null; +} | { + __kind__: "InvalidMetadata"; + InvalidMetadata: string; +} { + return "PasskeyWithThisPublicKeyIsAlreadyUsed" in value ? { + __kind__: "PasskeyWithThisPublicKeyIsAlreadyUsed", + PasskeyWithThisPublicKeyIsAlreadyUsed: value.PasskeyWithThisPublicKeyIsAlreadyUsed + } : "AuthnMethodNotFound" in value ? { + __kind__: "AuthnMethodNotFound", + AuthnMethodNotFound: value.AuthnMethodNotFound + } : "InvalidMetadata" in value ? { + __kind__: "InvalidMetadata", + InvalidMetadata: value.InvalidMetadata + } : value; +} +function from_candid_variant_n55(value: { + Ok: null; +} | { + Err: _AuthnMethodSecuritySettingsReplaceError; +}): { + __kind__: "Ok"; + Ok: null; +} | { + __kind__: "Err"; + Err: AuthnMethodSecuritySettingsReplaceError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: value.Ok + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_AuthnMethodSecuritySettingsReplaceError_n56(value.Err) + } : value; +} +function from_candid_variant_n57(value: { + AuthnMethodNotFound: null; +}): AuthnMethodSecuritySettingsReplaceError { + return "AuthnMethodNotFound" in value ? AuthnMethodSecuritySettingsReplaceError.AuthnMethodNotFound : value; +} +function from_candid_variant_n63(value: { + Ok: _IdRegNextStepResult; +} | { + Err: _CheckCaptchaError; +}): { + __kind__: "Ok"; + Ok: IdRegNextStepResult; +} | { + __kind__: "Err"; + Err: CheckCaptchaError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: from_candid_IdRegNextStepResult_n64(value.Ok) + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_CheckCaptchaError_n68(value.Err) + } : value; +} +function from_candid_variant_n67(value: { + CheckCaptcha: { + captcha_png_base64: string; + }; +} | { + Finish: null; +}): { + __kind__: "CheckCaptcha"; + CheckCaptcha: { + captcha_png_base64: string; + }; +} | { + __kind__: "Finish"; + Finish: null; +} { + return "CheckCaptcha" in value ? { + __kind__: "CheckCaptcha", + CheckCaptcha: value.CheckCaptcha + } : "Finish" in value ? { + __kind__: "Finish", + Finish: value.Finish + } : value; +} +function from_candid_variant_n69(value: { + NoRegistrationFlow: null; +} | { + UnexpectedCall: { + next_step: _RegistrationFlowNextStep; + }; +} | { + WrongSolution: { + new_captcha_png_base64: string; + }; +}): { + __kind__: "NoRegistrationFlow"; + NoRegistrationFlow: null; +} | { + __kind__: "UnexpectedCall"; + UnexpectedCall: { + next_step: RegistrationFlowNextStep; + }; +} | { + __kind__: "WrongSolution"; + WrongSolution: { + new_captcha_png_base64: string; + }; +} { + return "NoRegistrationFlow" in value ? { + __kind__: "NoRegistrationFlow", + NoRegistrationFlow: value.NoRegistrationFlow + } : "UnexpectedCall" in value ? { + __kind__: "UnexpectedCall", + UnexpectedCall: from_candid_record_n65(value.UnexpectedCall) + } : "WrongSolution" in value ? { + __kind__: "WrongSolution", + WrongSolution: value.WrongSolution + } : value; +} +function from_candid_variant_n81(value: { + Plausible: { + domain: [] | [string]; + track_localhost: [] | [boolean]; + hash_mode: [] | [boolean]; + api_host: [] | [string]; + }; +}): { + __kind__: "Plausible"; + Plausible: { + domain?: string; + track_localhost?: boolean; + hash_mode?: boolean; + api_host?: string; + }; +} { + return "Plausible" in value ? { + __kind__: "Plausible", + Plausible: from_candid_record_n82(value.Plausible) + } : value; +} +function from_candid_variant_n89(value: { + Google: null; +} | { + Unknown: null; +} | { + Microsoft: null; +}): OpenIdEmailVerification { + return "Google" in value ? OpenIdEmailVerification.Google : "Unknown" in value ? OpenIdEmailVerification.Unknown : "Microsoft" in value ? OpenIdEmailVerification.Microsoft : value; +} +function from_candid_variant_n93(value: { + Dynamic: { + reference_rate_sampling_interval_s: bigint; + threshold_pct: number; + current_rate_sampling_interval_s: bigint; + }; +} | { + Static: { + CaptchaDisabled: null; + } | { + CaptchaEnabled: null; + }; +}): { + __kind__: "Dynamic"; + Dynamic: { + reference_rate_sampling_interval_s: bigint; + threshold_pct: number; + current_rate_sampling_interval_s: bigint; + }; +} | { + __kind__: "Static"; + Static: Variant_CaptchaDisabled_CaptchaEnabled; +} { + return "Dynamic" in value ? { + __kind__: "Dynamic", + Dynamic: value.Dynamic + } : "Static" in value ? { + __kind__: "Static", + Static: from_candid_variant_n94(value.Static) + } : value; +} +function from_candid_variant_n94(value: { + CaptchaDisabled: null; +} | { + CaptchaEnabled: null; +}): Variant_CaptchaDisabled_CaptchaEnabled { + return "CaptchaDisabled" in value ? Variant_CaptchaDisabled_CaptchaEnabled.CaptchaDisabled : "CaptchaEnabled" in value ? Variant_CaptchaDisabled_CaptchaEnabled.CaptchaEnabled : value; +} +function from_candid_variant_n98(value: { + Ok: _AccountInfo; +} | { + Err: _CreateAccountError; +}): { + __kind__: "Ok"; + Ok: AccountInfo; +} | { + __kind__: "Err"; + Err: CreateAccountError; +} { + return "Ok" in value ? { + __kind__: "Ok", + Ok: from_candid_AccountInfo_n99(value.Ok) + } : "Err" in value ? { + __kind__: "Err", + Err: from_candid_CreateAccountError_n102(value.Err) + } : value; +} +function from_candid_vec_n116(value: Array<_AccountInfo>): Array { + return value.map((x)=>from_candid_AccountInfo_n99(x)); +} +function from_candid_vec_n121(value: Array<_DeviceWithUsage>): Array { + return value.map((x)=>from_candid_DeviceWithUsage_n122(x)); +} +function from_candid_vec_n126(value: Array<[string, { + map: _MetadataMap; + } | { + string: string; + } | { + bytes: Uint8Array; + }]>): Array<[string, { + __kind__: "map"; + map: MetadataMap; + } | { + __kind__: "string"; + string: string; + } | { + __kind__: "bytes"; + bytes: Uint8Array; + }]> { + return value.map((x)=>from_candid_tuple_n127(x)); +} +function from_candid_vec_n138(value: Array<_OpenIdCredential>): Array { + return value.map((x)=>from_candid_OpenIdCredential_n139(x)); +} +function from_candid_vec_n142(value: Array<[string, { + Map: _MetadataMapV2; + } | { + String: string; + } | { + Bytes: Uint8Array; + }]>): Array<[string, { + __kind__: "Map"; + Map: MetadataMapV2; + } | { + __kind__: "String"; + String: string; + } | { + __kind__: "Bytes"; + Bytes: Uint8Array; + }]> { + return value.map((x)=>from_candid_tuple_n143(x)); +} +function from_candid_vec_n173(value: Array<_AuthnMethod>): Array { + return value.map((x)=>from_candid_AuthnMethod_n174(x)); +} +function from_candid_vec_n181(value: Array<_AuthnMethodData>): Array { + return value.map((x)=>from_candid_AuthnMethodData_n182(x)); +} +function from_candid_vec_n211(value: Array<_DeviceData>): Array { + return value.map((x)=>from_candid_DeviceData_n149(x)); +} +function from_candid_vec_n84(value: Array<_OpenIdConfig>): Array { + return value.map((x)=>from_candid_OpenIdConfig_n85(x)); +} +function to_candid_AccountUpdate_n242(value: AccountUpdate): _AccountUpdate { + return to_candid_record_n199(value); +} +function to_candid_AuthnMethodData_n15(value: AuthnMethodData): _AuthnMethodData { + return to_candid_record_n16(value); +} +function to_candid_AuthnMethodProtection_n19(value: AuthnMethodProtection): _AuthnMethodProtection { + return to_candid_variant_n20(value); +} +function to_candid_AuthnMethodPurpose_n21(value: AuthnMethodPurpose): _AuthnMethodPurpose { + return to_candid_variant_n22(value); +} +function to_candid_AuthnMethodSecuritySettings_n17(value: AuthnMethodSecuritySettings): _AuthnMethodSecuritySettings { + return to_candid_record_n18(value); +} +function to_candid_AuthnMethod_n27(value: AuthnMethod): _AuthnMethod { + return to_candid_variant_n28(value); +} +function to_candid_DeviceData_n1(value: DeviceData): _DeviceData { + return to_candid_record_n2(value); +} +function to_candid_DeviceProtection_n7(value: DeviceProtection): _DeviceProtection { + return to_candid_variant_n8(value); +} +function to_candid_GetAttributesRequest_n151(value: GetAttributesRequest): _GetAttributesRequest { + return to_candid_record_n152(value); +} +function to_candid_HttpRequest_n166(value: HttpRequest): _HttpRequest { + return to_candid_record_n167(value); +} +function to_candid_IdRegFinishArg_n203(value: IdRegFinishArg): _IdRegFinishArg { + return to_candid_record_n204(value); +} +function to_candid_IdentityPropertiesReplace_n198(value: IdentityPropertiesReplace): _IdentityPropertiesReplace { + return to_candid_record_n199(value); +} +function to_candid_KeyType_n9(value: KeyType): _KeyType { + return to_candid_variant_n10(value); +} +function to_candid_MetadataMapV2_n23(value: MetadataMapV2): _MetadataMapV2 { + return to_candid_vec_n24(value); +} +function to_candid_MetadataMap_n3(value: MetadataMap): _MetadataMap { + return to_candid_vec_n4(value); +} +function to_candid_PrepareAttributeRequest_n226(value: PrepareAttributeRequest): _PrepareAttributeRequest { + return to_candid_record_n227(value); +} +function to_candid_Purpose_n11(value: Purpose): _Purpose { + return to_candid_variant_n12(value); +} +function to_candid_WebAuthn_n29(value: WebAuthn): _WebAuthn { + return to_candid_record_n30(value); +} +function to_candid_opt_n106(value: AccountNumber | null): [] | [_AccountNumber] { + return value === null ? candid_none() : candid_some(value); +} +function to_candid_opt_n224(value: bigint | null): [] | [bigint] { + return value === null ? candid_none() : candid_some(value); +} +function to_candid_opt_n232(value: Principal | null): [] | [Principal] { + return value === null ? candid_none() : candid_some(value); +} +function to_candid_opt_n43(value: RegistrationId | null): [] | [_RegistrationId] { + return value === null ? candid_none() : candid_some(value); +} +function to_candid_opt_n47(value: AuthnMethodData | null): [] | [_AuthnMethodData] { + return value === null ? candid_none() : candid_some(to_candid_AuthnMethodData_n15(value)); +} +function to_candid_record_n152(value: { + origin: FrontendHostname; + account_number?: AccountNumber; + attributes: Array<[string, Uint8Array]>; + issued_at_timestamp_ns: Timestamp; + identity_number: IdentityNumber; +}): { + origin: _FrontendHostname; + account_number: [] | [_AccountNumber]; + attributes: Array<[string, Uint8Array]>; + issued_at_timestamp_ns: _Timestamp; + identity_number: _IdentityNumber; +} { + return { + origin: value.origin, + account_number: value.account_number ? candid_some(value.account_number) : candid_none(), + attributes: value.attributes, + issued_at_timestamp_ns: value.issued_at_timestamp_ns, + identity_number: value.identity_number + }; +} +function to_candid_record_n16(value: { + security_settings: AuthnMethodSecuritySettings; + metadata: MetadataMapV2; + last_authentication?: Timestamp; + authn_method: AuthnMethod; +}): { + security_settings: _AuthnMethodSecuritySettings; + metadata: _MetadataMapV2; + last_authentication: [] | [_Timestamp]; + authn_method: _AuthnMethod; +} { + return { + security_settings: to_candid_AuthnMethodSecuritySettings_n17(value.security_settings), + metadata: to_candid_MetadataMapV2_n23(value.metadata), + last_authentication: value.last_authentication ? candid_some(value.last_authentication) : candid_none(), + authn_method: to_candid_AuthnMethod_n27(value.authn_method) + }; +} +function to_candid_record_n167(value: { + url: string; + method: string; + body: Uint8Array; + headers: Array; + certificate_version?: number; +}): { + url: string; + method: string; + body: Uint8Array; + headers: Array<_HeaderField>; + certificate_version: [] | [number]; +} { + return { + url: value.url, + method: value.method, + body: value.body, + headers: value.headers, + certificate_version: value.certificate_version ? candid_some(value.certificate_version) : candid_none() + }; +} +function to_candid_record_n18(value: { + protection: AuthnMethodProtection; + purpose: AuthnMethodPurpose; +}): { + protection: _AuthnMethodProtection; + purpose: _AuthnMethodPurpose; +} { + return { + protection: to_candid_AuthnMethodProtection_n19(value.protection), + purpose: to_candid_AuthnMethodPurpose_n21(value.purpose) + }; +} +function to_candid_record_n199(value: { + name?: string; +}): { + name: [] | [string]; +} { + return { + name: value.name ? candid_some(value.name) : candid_none() + }; +} +function to_candid_record_n2(value: { + alias: string; + metadata?: MetadataMap; + origin?: string; + protection: DeviceProtection; + pubkey: DeviceKey; + key_type: KeyType; + aaguid?: Aaguid; + purpose: Purpose; + credential_id?: CredentialId; +}): { + alias: string; + metadata: [] | [_MetadataMap]; + origin: [] | [string]; + protection: _DeviceProtection; + pubkey: _DeviceKey; + key_type: _KeyType; + aaguid: [] | [_Aaguid]; + purpose: _Purpose; + credential_id: [] | [_CredentialId]; +} { + return { + alias: value.alias, + metadata: value.metadata ? candid_some(to_candid_MetadataMap_n3(value.metadata)) : candid_none(), + origin: value.origin ? candid_some(value.origin) : candid_none(), + protection: to_candid_DeviceProtection_n7(value.protection), + pubkey: value.pubkey, + key_type: to_candid_KeyType_n9(value.key_type), + aaguid: value.aaguid ? candid_some(value.aaguid) : candid_none(), + purpose: to_candid_Purpose_n11(value.purpose), + credential_id: value.credential_id ? candid_some(value.credential_id) : candid_none() + }; +} +function to_candid_record_n204(value: { + name?: string; + authn_method: AuthnMethodData; +}): { + name: [] | [string]; + authn_method: _AuthnMethodData; +} { + return { + name: value.name ? candid_some(value.name) : candid_none(), + authn_method: to_candid_AuthnMethodData_n15(value.authn_method) + }; +} +function to_candid_record_n227(value: { + origin: FrontendHostname; + attribute_keys: Array; + account_number?: AccountNumber; + identity_number: IdentityNumber; +}): { + origin: _FrontendHostname; + attribute_keys: Array; + account_number: [] | [_AccountNumber]; + identity_number: _IdentityNumber; +} { + return { + origin: value.origin, + attribute_keys: value.attribute_keys, + account_number: value.account_number ? candid_some(value.account_number) : candid_none(), + identity_number: value.identity_number + }; +} +function to_candid_record_n30(value: { + pubkey: PublicKey; + aaguid?: Aaguid; + credential_id: CredentialId; +}): { + pubkey: _PublicKey; + aaguid: [] | [_Aaguid]; + credential_id: _CredentialId; +} { + return { + pubkey: value.pubkey, + aaguid: value.aaguid ? candid_some(value.aaguid) : candid_none(), + credential_id: value.credential_id + }; +} +function to_candid_tuple_n25(value: [string, { + __kind__: "Map"; + Map: MetadataMapV2; + } | { + __kind__: "String"; + String: string; + } | { + __kind__: "Bytes"; + Bytes: Uint8Array; + }]): [string, { + Map: _MetadataMapV2; + } | { + String: string; + } | { + Bytes: Uint8Array; + }] { + return [ + value[0], + to_candid_variant_n26(value[1]) + ]; +} +function to_candid_tuple_n5(value: [string, { + __kind__: "map"; + map: MetadataMap; + } | { + __kind__: "string"; + string: string; + } | { + __kind__: "bytes"; + bytes: Uint8Array; + }]): [string, { + map: _MetadataMap; + } | { + string: string; + } | { + bytes: Uint8Array; + }] { + return [ + value[0], + to_candid_variant_n6(value[1]) + ]; +} +function to_candid_variant_n10(value: KeyType): { + platform: null; +} | { + seed_phrase: null; +} | { + cross_platform: null; +} | { + unknown: null; +} | { + browser_storage_key: null; +} { + return value == KeyType.platform ? { + platform: null + } : value == KeyType.seed_phrase ? { + seed_phrase: null + } : value == KeyType.cross_platform ? { + cross_platform: null + } : value == KeyType.unknown ? { + unknown_: null + } : value == KeyType.browser_storage_key ? { + browser_storage_key: null + } : value; +} +function to_candid_variant_n12(value: Purpose): { + authentication: null; +} | { + recovery: null; +} { + return value == Purpose.authentication ? { + authentication: null + } : value == Purpose.recovery ? { + recovery: null + } : value; +} +function to_candid_variant_n20(value: AuthnMethodProtection): { + Protected: null; +} | { + Unprotected: null; +} { + return value == AuthnMethodProtection.Protected ? { + Protected: null + } : value == AuthnMethodProtection.Unprotected ? { + Unprotected: null + } : value; +} +function to_candid_variant_n22(value: AuthnMethodPurpose): { + Recovery: null; +} | { + Authentication: null; +} { + return value == AuthnMethodPurpose.Recovery ? { + Recovery: null + } : value == AuthnMethodPurpose.Authentication ? { + Authentication: null + } : value; +} +function to_candid_variant_n26(value: { + __kind__: "Map"; + Map: MetadataMapV2; +} | { + __kind__: "String"; + String: string; +} | { + __kind__: "Bytes"; + Bytes: Uint8Array; +}): { + Map: _MetadataMapV2; +} | { + String: string; +} | { + Bytes: Uint8Array; +} { + return value.__kind__ === "Map" ? { + Map: to_candid_MetadataMapV2_n23(value.Map) + } : value.__kind__ === "String" ? { + String: value.String + } : value.__kind__ === "Bytes" ? { + Bytes: value.Bytes + } : value; +} +function to_candid_variant_n28(value: { + __kind__: "PubKey"; + PubKey: PublicKeyAuthn; +} | { + __kind__: "WebAuthn"; + WebAuthn: WebAuthn; +}): { + PubKey: _PublicKeyAuthn; +} | { + WebAuthn: _WebAuthn; +} { + return value.__kind__ === "PubKey" ? { + PubKey: value.PubKey + } : value.__kind__ === "WebAuthn" ? { + WebAuthn: to_candid_WebAuthn_n29(value.WebAuthn) + } : value; +} +function to_candid_variant_n6(value: { + __kind__: "map"; + map: MetadataMap; +} | { + __kind__: "string"; + string: string; +} | { + __kind__: "bytes"; + bytes: Uint8Array; +}): { + map: _MetadataMap; +} | { + string: string; +} | { + bytes: Uint8Array; +} { + return value.__kind__ === "map" ? { + map: to_candid_MetadataMap_n3(value.map) + } : value.__kind__ === "string" ? { + string: value.string + } : value.__kind__ === "bytes" ? { + bytes: value.bytes + } : value; +} +function to_candid_variant_n8(value: DeviceProtection): { + unprotected: null; +} | { + protected: null; +} { + return value == DeviceProtection.unprotected ? { + unprotected: null + } : value == DeviceProtection.protected ? { + protected_: null + } : value; +} +function to_candid_vec_n24(value: Array<[string, { + __kind__: "Map"; + Map: MetadataMapV2; + } | { + __kind__: "String"; + String: string; + } | { + __kind__: "Bytes"; + Bytes: Uint8Array; + }]>): Array<[string, { + Map: _MetadataMapV2; + } | { + String: string; + } | { + Bytes: Uint8Array; + }]> { + return value.map((x)=>to_candid_tuple_n25(x)); +} +function to_candid_vec_n4(value: Array<[string, { + __kind__: "map"; + map: MetadataMap; + } | { + __kind__: "string"; + string: string; + } | { + __kind__: "bytes"; + bytes: Uint8Array; + }]>): Array<[string, { + map: _MetadataMap; + } | { + string: string; + } | { + bytes: Uint8Array; + }]> { + return value.map((x)=>to_candid_tuple_n5(x)); +} +export interface CreateActorOptions { + agent?: Agent; + agentOptions?: HttpAgentOptions; + actorOptions?: ActorConfig; +} +export function createActor(canisterId: string, options: CreateActorOptions = {}): Internet_identity { + const agent = options.agent || HttpAgent.createSync({ + ...options.agentOptions + }); + if (options.agent && options.agentOptions) { + console.warn("Detected both agent and agentOptions passed to createActor. Ignoring agentOptions and proceeding with the provided agent."); + } + const actor = Actor.createActor<_SERVICE>(idlFactory, { + agent, + canisterId: canisterId, + ...options.actorOptions + }); + return new Internet_identity(actor); +} diff --git a/examples/t_ecdsa/frontend/src/bindings/t_ecdsa.ts b/examples/t_ecdsa/frontend/src/bindings/t_ecdsa.ts new file mode 100644 index 0000000..0328082 --- /dev/null +++ b/examples/t_ecdsa/frontend/src/bindings/t_ecdsa.ts @@ -0,0 +1,133 @@ +/* eslint-disable */ + +// @ts-nocheck + +// This file was automatically generated by @icp-sdk/bindgen@0.2.2. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Actor, HttpAgent, type HttpAgentOptions, type ActorConfig, type Agent, type ActorSubclass } from "@icp-sdk/core/agent"; +import type { Principal } from "@icp-sdk/core/principal"; +import { idlFactory, type _SERVICE } from "./declarations/t_ecdsa.did"; +export interface Some { + __kind__: "Some"; + value: T; +} +export interface None { + __kind__: "None"; +} +export type Option = Some | None; +function some(value: T): Some { + return { + __kind__: "Some", + value: value + }; +} +function none(): None { + return { + __kind__: "None" + }; +} +function isNone(option: Option): option is None { + return option.__kind__ === "None"; +} +function isSome(option: Option): option is Some { + return option.__kind__ === "Some"; +} +function unwrap(option: Option): T { + if (isNone(option)) { + throw new Error("unwrap: none"); + } + return option.value; +} +function candid_some(value: T): [T] { + return [ + value + ]; +} +function candid_none(): [] { + return []; +} +function record_opt_to_undefined(arg: T | null): T | undefined { + return arg == null ? undefined : arg; +} +export interface t_ecdsaInterface { + getEvmAddress(): Promise; + getNewPublicKey(): Promise; + getPublicKey(): Promise; + signWithEcdsa(arg0: string): Promise; + signWithEthereum(mesage: string): Promise; + signWithEvmWallet(arg0: Uint8Array): Promise; + verifyWithEcdsa(arg0: { + signature: string; + publicKey: string; + message: string; + }): Promise; + verifyWithEthereum(arg0: { + signature: string; + message: string; + ethereumAddress: string; + }): Promise; +} +export class T_ecdsa implements t_ecdsaInterface { + constructor(private actor: ActorSubclass<_SERVICE>){} + async getEvmAddress(): Promise { + const result = await this.actor.getEvmAddress(); + return result; + } + async getNewPublicKey(): Promise { + const result = await this.actor.getNewPublicKey(); + return result; + } + async getPublicKey(): Promise { + const result = await this.actor.getPublicKey(); + return result; + } + async signWithEcdsa(arg0: string): Promise { + const result = await this.actor.signWithEcdsa(arg0); + return result; + } + async signWithEthereum(arg0: string): Promise { + const result = await this.actor.signWithEthereum(arg0); + return result; + } + async signWithEvmWallet(arg0: Uint8Array): Promise { + const result = await this.actor.signWithEvmWallet(arg0); + return result; + } + async verifyWithEcdsa(arg0: { + signature: string; + publicKey: string; + message: string; + }): Promise { + const result = await this.actor.verifyWithEcdsa(arg0); + return result; + } + async verifyWithEthereum(arg0: { + signature: string; + message: string; + ethereumAddress: string; + }): Promise { + const result = await this.actor.verifyWithEthereum(arg0); + return result; + } +} +export interface CreateActorOptions { + agent?: Agent; + agentOptions?: HttpAgentOptions; + actorOptions?: ActorConfig; +} +export function createActor(canisterId: string, options: CreateActorOptions = {}): T_ecdsa { + const agent = options.agent || HttpAgent.createSync({ + ...options.agentOptions + }); + if (options.agent && options.agentOptions) { + console.warn("Detected both agent and agentOptions passed to createActor. Ignoring agentOptions and proceeding with the provided agent."); + } + const actor = Actor.createActor<_SERVICE>(idlFactory, { + agent, + canisterId: canisterId, + ...options.actorOptions + }); + return new T_ecdsa(actor); +} diff --git a/examples/t_ecdsa/frontend/src/bindings/t_ecdsa_backend.ts b/examples/t_ecdsa/frontend/src/bindings/t_ecdsa_backend.ts new file mode 100644 index 0000000..354bce6 --- /dev/null +++ b/examples/t_ecdsa/frontend/src/bindings/t_ecdsa_backend.ts @@ -0,0 +1,133 @@ +/* eslint-disable */ + +// @ts-nocheck + +// This file was automatically generated by @icp-sdk/bindgen@0.2.2. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Actor, HttpAgent, type HttpAgentOptions, type ActorConfig, type Agent, type ActorSubclass } from "@icp-sdk/core/agent"; +import type { Principal } from "@icp-sdk/core/principal"; +import { idlFactory, type _SERVICE } from "./declarations/t_ecdsa_backend.did"; +export interface Some { + __kind__: "Some"; + value: T; +} +export interface None { + __kind__: "None"; +} +export type Option = Some | None; +function some(value: T): Some { + return { + __kind__: "Some", + value: value + }; +} +function none(): None { + return { + __kind__: "None" + }; +} +function isNone(option: Option): option is None { + return option.__kind__ === "None"; +} +function isSome(option: Option): option is Some { + return option.__kind__ === "Some"; +} +function unwrap(option: Option): T { + if (isNone(option)) { + throw new Error("unwrap: none"); + } + return option.value; +} +function candid_some(value: T): [T] { + return [ + value + ]; +} +function candid_none(): [] { + return []; +} +function record_opt_to_undefined(arg: T | null): T | undefined { + return arg == null ? undefined : arg; +} +export interface t_ecdsa_backendInterface { + getEvmAddress(): Promise; + getNewPublicKey(): Promise; + getPublicKey(): Promise; + signWithEcdsa(arg0: string): Promise; + signWithEthereum(mesage: string): Promise; + signWithEvmWallet(arg0: Uint8Array): Promise; + verifyWithEcdsa(arg0: { + signature: string; + publicKey: string; + message: string; + }): Promise; + verifyWithEthereum(arg0: { + signature: string; + message: string; + ethereumAddress: string; + }): Promise; +} +export class T_ecdsa_backend implements t_ecdsa_backendInterface { + constructor(private actor: ActorSubclass<_SERVICE>){} + async getEvmAddress(): Promise { + const result = await this.actor.getEvmAddress(); + return result; + } + async getNewPublicKey(): Promise { + const result = await this.actor.getNewPublicKey(); + return result; + } + async getPublicKey(): Promise { + const result = await this.actor.getPublicKey(); + return result; + } + async signWithEcdsa(arg0: string): Promise { + const result = await this.actor.signWithEcdsa(arg0); + return result; + } + async signWithEthereum(arg0: string): Promise { + const result = await this.actor.signWithEthereum(arg0); + return result; + } + async signWithEvmWallet(arg0: Uint8Array): Promise { + const result = await this.actor.signWithEvmWallet(arg0); + return result; + } + async verifyWithEcdsa(arg0: { + signature: string; + publicKey: string; + message: string; + }): Promise { + const result = await this.actor.verifyWithEcdsa(arg0); + return result; + } + async verifyWithEthereum(arg0: { + signature: string; + message: string; + ethereumAddress: string; + }): Promise { + const result = await this.actor.verifyWithEthereum(arg0); + return result; + } +} +export interface CreateActorOptions { + agent?: Agent; + agentOptions?: HttpAgentOptions; + actorOptions?: ActorConfig; +} +export function createActor(canisterId: string, options: CreateActorOptions = {}): T_ecdsa_backend { + const agent = options.agent || HttpAgent.createSync({ + ...options.agentOptions + }); + if (options.agent && options.agentOptions) { + console.warn("Detected both agent and agentOptions passed to createActor. Ignoring agentOptions and proceeding with the provided agent."); + } + const actor = Actor.createActor<_SERVICE>(idlFactory, { + agent, + canisterId: canisterId, + ...options.actorOptions + }); + return new T_ecdsa_backend(actor); +} diff --git a/examples/t_ecdsa/frontend/src/hooks/client.ts b/examples/t_ecdsa/frontend/src/hooks/client.ts new file mode 100644 index 0000000..61112f3 --- /dev/null +++ b/examples/t_ecdsa/frontend/src/hooks/client.ts @@ -0,0 +1,7 @@ +import { createPublicClient, http } from 'viem'; +import { anvil } from 'viem/chains'; + +export const publicClient = createPublicClient({ + chain: anvil, + transport: http('http://localhost:8545'), +}) \ No newline at end of file diff --git a/examples/t_ecdsa/frontend/src/hooks/icpAuth.ts b/examples/t_ecdsa/frontend/src/hooks/icpAuth.ts new file mode 100644 index 0000000..9df1a2c --- /dev/null +++ b/examples/t_ecdsa/frontend/src/hooks/icpAuth.ts @@ -0,0 +1,154 @@ +import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; +import { AuthClient } from '@icp-sdk/auth/client'; +import { + canisterId as INTERNET_IDENTITY_CANISTER_ID, +} from '../../../declarations/internet_identity'; + +const network = import.meta.env.VITE_DFX_NETWORK || 'local'; +export const identityProvider = + network === 'ic' + ? 'https://identity.ic0.app' + : `http://${INTERNET_IDENTITY_CANISTER_ID}.localhost:4943`; + +export interface UseIcpAuthResult { + authClient: AuthClient | null; + identityProvider: string; + isAuthenticated: boolean; + isLoading: boolean; + principal: string | null; + login: () => Promise; + logout: () => Promise; + refresh: () => Promise; +} + +export const useIcpAuth = (): UseIcpAuthResult => { + const [authClient, setAuthClient] = useState(null); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [principal, setPrincipal] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const clientPromiseRef = useRef | null>(null); + + const refreshSession = useCallback(async (client: AuthClient) => { + try { + const authenticated = await client.isAuthenticated(); + setIsAuthenticated(authenticated); + if (authenticated) { + const currentPrincipal = client.getIdentity().getPrincipal().toString(); + setPrincipal(currentPrincipal); + } else { + setPrincipal(null); + } + } catch (error) { + console.error('Failed to refresh authentication session:', error); + setIsAuthenticated(false); + setPrincipal(null); + } + }, []); + + const ensureAuthClient = useCallback(async (): Promise => { + if (authClient) { + return authClient; + } + + if (typeof window === 'undefined') { + throw new Error('Auth client is only available in the browser.'); + } + + if (!clientPromiseRef.current) { + clientPromiseRef.current = AuthClient.create(); + } + + try { + const client = await clientPromiseRef.current; + setAuthClient((current) => current ?? client); + return client; + } catch (error) { + clientPromiseRef.current = null; + throw error; + } + }, [authClient]); + + useEffect(() => { + if (typeof window === 'undefined') { + setIsLoading(false); + return; + } + + let cancelled = false; + + ensureAuthClient() + .then((client) => { + if (cancelled) { + return; + } + return refreshSession(client); + }) + .catch((error) => { + if (!cancelled) { + console.error('Failed to initialise auth client:', error); + } + }) + .finally(() => { + if (!cancelled) { + setIsLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [ensureAuthClient, refreshSession]); + + const login = useCallback(async () => { + try { + const client = await ensureAuthClient(); + + return await new Promise((resolve) => { + client.login({ + identityProvider, + onSuccess: async () => { + await refreshSession(client); + resolve(true); + }, + onError: (error) => { + console.error('Login failed:', error); + resolve(false); + }, + }); + }); + } catch (error) { + console.error('Failed to start login flow:', error); + return false; + } + }, [ensureAuthClient, refreshSession]); + + const logout = useCallback(async () => { + try { + const client = await ensureAuthClient(); + await client.logout(); + await refreshSession(client); + } catch (error) { + console.error('Logout failed:', error); + } + }, [ensureAuthClient, refreshSession]); + + const refresh = useCallback(async () => { + try { + const client = await ensureAuthClient(); + await refreshSession(client); + } catch (error) { + console.error('Failed to refresh authentication state:', error); + } + }, [ensureAuthClient, refreshSession]); + + return { + authClient, + identityProvider, + isAuthenticated, + isLoading, + principal, + login, + logout, + refresh, + }; +}; diff --git a/examples/t_ecdsa/frontend/src/hooks/icpWalletClient.ts b/examples/t_ecdsa/frontend/src/hooks/icpWalletClient.ts new file mode 100644 index 0000000..42b9d5a --- /dev/null +++ b/examples/t_ecdsa/frontend/src/hooks/icpWalletClient.ts @@ -0,0 +1,192 @@ +import { + type Address, + type Chain, + type Hex, + type LocalAccount, + type RpcSchema, + type SerializeTransactionFn, + type SignableMessage, + type Signature, + type TransactionSerializable, + type TypedData, + type TypedDataDefinition, + type WalletClient, + type HttpTransport, + createWalletClient, + http, + keccak256, + serializeTransaction, + hashMessage, + serializeSignature, + hexToBytes, +} from 'viem'; +import { toAccount } from 'viem/accounts'; +import { AuthClient } from '@icp-sdk/auth/client'; +import { HttpAgent } from '@icp-sdk/core/agent'; +import { createActor } from "../bindings/t_ecdsa_backend"; + + +const toIcpAccount = async (authClient: AuthClient): Promise => { + const canisterId = process.env.CANISTER_ID_T_ECDSA_BACKEND; + if (!canisterId) { + throw new Error('CANISTER_ID_T_ECDSA_BACKEND is not set'); + } + // 開発時はViteプロキシ経由で同一オリジンにし、本番はレプリカ直指定 + const isLocal = + process.env.DFX_NETWORK === 'local' || + typeof process !== 'undefined' && + process.env?.NODE_ENV === 'development'; + const host = + typeof window !== 'undefined' && isLocal + ? window.location.origin // Viteの/apiプロキシ経由でレプリカへ + : 'http://127.0.0.1:4943'; + const identity = authClient.getIdentity(); + const agent = await HttpAgent.create({ + identity, + host, + shouldFetchRootKey: isLocal, + }); + + const actor = createActor(canisterId, { agent }); + // getNewPublicKey で鍵を生成したあと getEvmAddress でアドレスを取得する(未生成のまま getEvmAddress するとキャニスターが reject する) + const publicKey = await actor.getNewPublicKey(); + console.log('publicKey: ', publicKey); + const rawAddress = await actor.getEvmAddress(); + console.log('rawAddress: ', rawAddress); + const address = (typeof rawAddress === 'string' ? rawAddress : String(rawAddress)).trim(); + console.log('address: ', address); + if (!address || address.length !== 42 || !/^0x[0-9a-fA-F]{40}$/.test(address)) { + throw new Error( + `Invalid EVM address from canister: "${rawAddress}". ` + + 'Ensure the local replica is running (dfx start) and the backend canister is deployed.' + ); + } + const evmAddress = address as Address; + + const signMessage = async ({ + message, + }: { + message: SignableMessage; + }): Promise => { + const hash = hashMessage(message); + const hashBytes = hexToBytes(hash); + const signature = await actor.signWithEvmWallet(hashBytes); + + // 0xプレフィックスを削除 + const sigHex = signature.startsWith('0x') ? signature.slice(2) : signature; + + // 署名は65バイト (130文字) である必要がある + if (sigHex.length !== 130) { + throw new Error(`Invalid signature length: ${sigHex.length}, expected 130`); + } + + const r = '0x' + sigHex.slice(0, 64); + const s = '0x' + sigHex.slice(64, 128); + let v = parseInt(sigHex.slice(128, 130), 16); + + // Ethereumの署名では、v値は27または28である必要がある + // recovery IDが0または1の場合、27を加算する + if (v < 27) { + v += 27; + } + + console.log('Parsed signature - r:', r, 's:', s, 'v:', v); + + const sig: Signature = { + r: r as Hex, + s: s as Hex, + v: BigInt(v), + }; + + return serializeSignature(sig); + }; + + const signTransaction = async< + TTransactionSerializable extends TransactionSerializable, + >( + transaction: TTransactionSerializable, + args?: { + serializer?: SerializeTransactionFn; + } + ): Promise => { + if (!args?.serializer) { + return signTransaction(transaction, { + serializer: serializeTransaction, + }); + } + const serialized = args.serializer(transaction); + const hash = keccak256(serialized as Address); + const hashBytes = hexToBytes(hash); + const signature = await actor.signWithEvmWallet(hashBytes); + + // 0xプレフィックスを削除 + const sigHex = signature.startsWith('0x') ? signature.slice(2) : signature; + + // 署名は65バイト (130文字) である必要がある + if (sigHex.length !== 130) { + throw new Error(`Invalid signature length: ${sigHex.length}, expected 130`); + } + + const r = '0x' + sigHex.slice(0, 64); + const s = '0x' + sigHex.slice(64, 128); + let v = parseInt(sigHex.slice(128, 130), 16); + + // Ethereumの署名では、v値は27または28である必要がある + // recovery IDが0または1の場合、27を加算する + if (v < 27) { + v += 27; + } + + console.log('Parsed signature - r:', r, 's:', s, 'v:', v); + + const sig: Signature = { + r: r as Hex, + s: s as Hex, + v: BigInt(v), + }; + + return args.serializer(transaction, sig); + }; + + const signTypedData = async < + typedData extends TypedData | Record, + primaryType extends keyof typedData | 'EIP712Domain' = keyof typedData, + >( + _typedData: TypedDataDefinition, + ): Promise => { + throw new Error('Typed data signing is not supported yet for ICP wallet.'); + }; + + return toAccount({ + address: evmAddress, + signMessage, + signTransaction, + signTypedData, + }); +}; + +export interface CreateIcpWalletOptions { + authClient: AuthClient; + chain: Chain; + transport?: HttpTransport | undefined; +} + +// export type IcpWalletClient = WalletClient; +export type IcpWalletClient = WalletClient; + +export async function createIcpWalletClient( + options: CreateIcpWalletOptions, +): Promise { + if (!(await options.authClient.isAuthenticated())) { + throw new Error('Auth client must be authenticated before creating wallet client.'); + } + + const account = await toIcpAccount(options.authClient); + const walletClient = createWalletClient({ + account, + chain: options.chain, + transport: options.transport ?? http(), // transportがundefinedまたは未指定の場合はダミー値を使用 + }); + + return walletClient; +} diff --git a/examples/t_ecdsa/frontend/src/hooks/useCounterContract.ts b/examples/t_ecdsa/frontend/src/hooks/useCounterContract.ts new file mode 100644 index 0000000..3422c6c --- /dev/null +++ b/examples/t_ecdsa/frontend/src/hooks/useCounterContract.ts @@ -0,0 +1,39 @@ +import { useMemo } from 'preact/hooks'; +import { + getContract, + type Address, + type GetContractReturnType +} from 'viem'; +import { publicClient } from './client'; +import { type IcpWalletClient } from './icpWalletClient'; +import counterAbi from '../../../../../../solidity/out/Counter.sol/Counter.json'; + +const COUNTER_CONTRACT_ADDRESS: Address = '0x5FbDB2315678afecb367f032d93F642f64180aa3' as Address; + +export const useCounterContract = (walletClient: IcpWalletClient | null) => { + const contract = useMemo(() => { + if (!walletClient) { + // walletClientがない場合は、読み取り専用のcontractを返す + return getContract({ + address: COUNTER_CONTRACT_ADDRESS, + abi: counterAbi.abi, + client: publicClient, + }); + } + + // walletClientがある場合は、読み書き可能なcontractを返す + return getContract({ + address: COUNTER_CONTRACT_ADDRESS, + abi: counterAbi.abi, + client: { + public: publicClient, + wallet: walletClient, + }, + }); + }, [walletClient]); + + return { + contract, + contractAddress: COUNTER_CONTRACT_ADDRESS, + }; +}; diff --git a/examples/t_ecdsa/frontend/src/index.tsx b/examples/t_ecdsa/frontend/src/index.tsx new file mode 100644 index 0000000..78fc9c9 --- /dev/null +++ b/examples/t_ecdsa/frontend/src/index.tsx @@ -0,0 +1,217 @@ +import { hydrate, prerender as ssr } from 'preact-iso'; +import { useState, useEffect } from 'preact/hooks'; +import { Address, verifyMessage, http } from 'viem'; +import { anvil } from 'viem/chains'; +import { useIcpAuth } from './hooks/icpAuth'; +import { createIcpWalletClient, type IcpWalletClient } from './hooks/icpWalletClient'; +import { useCounterContract } from './hooks/useCounterContract'; + +export function App() { + const { authClient, isAuthenticated, isLoading, principal, login, logout } = useIcpAuth(); + const [walletClient, setWalletClient] = useState(null); + const [accountAddress, setAccountAddress] = useState
(null); + const [message, setMessage] = useState('hello world'); + const [signature, setSignature] = useState(null); + const [isValid, setIsValid] = useState(null); + const [counterValue, setCounterValue] = useState(null); + const [number, setNumber] = useState(null); + + // カスタムフックでcounterContractを管理 + const { contract: counterContract, contractAddress } = useCounterContract(walletClient); + + useEffect(() => { + if (!authClient || !isAuthenticated) { + setWalletClient(null); + setAccountAddress(null); + return; + } + + let cancelled = false; + + // 非同期関数を明示的に定義 + const initializeWallet = async () => { + try { + // ICPキャニスター経由で署名を行うLocalAccountを直接作成 + const walletClient = await createIcpWalletClient({ + authClient, + chain: anvil, + transport: http('http://localhost:8545'), + }); + + if (cancelled) { + return; + } + + setWalletClient(walletClient); + setAccountAddress(walletClient.account.address); + console.log('ICP Account created successfully:', walletClient.account.address); + } catch (error) { + if (!cancelled) { + console.error('Failed to create ICP account:', error); + setWalletClient(null); + setAccountAddress(null); + } + } + }; + + initializeWallet(); + + return () => { + cancelled = true; + }; + }, [authClient, isAuthenticated]); + + const handleAuth = async () => { + if (isAuthenticated) { + await logout(); + setWalletClient(null); + setAccountAddress(null); + setSignature(null); + setIsValid(null); + return; + } + + await login(); + }; + + const handleSign = async () => { + if (!walletClient || !accountAddress) { + return; + } + + try { + const signed = await walletClient.signMessage({ + account: walletClient.account, + message, + }); + setSignature(signed); + // クライアント側で署名検証 + const verified = await verifyMessage({ + address: accountAddress, + message, + signature: signed, + }); + setIsValid(verified); + } catch (error) { + console.error('Failed to sign message:', error); + setSignature(null); + setIsValid(null); + } + }; + + const readCounterValue = async () => { + if (!counterContract) { + return; + } + try { + const value = await counterContract.read.number(); + setCounterValue(value as bigint); + } catch (error) { + console.error('Failed to read counter value:', error); + setCounterValue(null); + } + }; + + const handleIncrement = async () => { + if (!walletClient || !counterContract) { + return; + } + try { + const hash = await counterContract.write.increment(); + console.log('Transaction hash:', hash); + // インクリメント後にカウンター値を再読み込み + await readCounterValue(); + } catch (error) { + console.error('Failed to increment counter:', error); + } + }; + + const handleSetNumber = async () => { + if (!walletClient || !counterContract || number === null) { + return; + } + try { + const hash = await counterContract.write.setNumber([number]); + console.log('Transaction hash:', hash); + // 設定後にカウンター値を再読み込み + await readCounterValue(); + } catch (error) { + console.error('Failed to set number:', error); + } + }; + + // BigInt変換のエラーハンドリングを追加 + const handleNumberInput = (e: Event) => { + const target = e.target as HTMLInputElement; + const value = target.value.trim(); + + if (value === '') { + setNumber(null); + return; + } + + try { + setNumber(BigInt(value)); + } catch (error) { + console.error('Invalid number input:', error); + // 無効な入力の場合は現在の値を保持 + } + }; + + return ( +
+
+

T-ECDSA

+ +

Status: {isLoading ? 'Checking…' : isAuthenticated ? 'Authenticated' : 'Not authenticated'}

+ {isAuthenticated && ( + <> +

Principal: {principal}

+

Account Address: {accountAddress}

+

Counter Contract: {contractAddress}

+

Contract Ready: {counterContract ? 'Yes' : 'No'}

+ + )} +
+ setMessage((e.target as HTMLInputElement).value)} + value={message} + /> + +

Signature: {signature}

+

isValid: {isValid !== null ? isValid.toString() : 'Not verified yet'}

+
+ + + + +

Counter: {counterValue !== null ? counterValue.toString() : 'Not read yet'}

+
+
+ ); +} + +if (typeof window !== 'undefined') { + hydrate(, document.getElementById('app')); +} + +export async function prerender(data) { + return await ssr(); +} diff --git a/examples/t_ecdsa/frontend/src/test/README.md b/examples/t_ecdsa/frontend/src/test/README.md new file mode 100644 index 0000000..8e47fca --- /dev/null +++ b/examples/t_ecdsa/frontend/src/test/README.md @@ -0,0 +1,102 @@ +# Ethereum署名テスト + +このディレクトリには、ICPキャニスターのEthereum署名機能をviemライブラリを使用してテストするためのTypeScriptテストが含まれています。 + +## テストの概要 + +テストは以下の機能を検証します: + +1. **ウォレットアドレスの取得**: `getEvmAddress`関数でEthereum形式のアドレスを取得 +2. **メッセージの署名**: `signWithEthereum`関数でメッセージに署名 +3. **署名の検証**: viemの`verifyMessage`関数で署名を検証 +4. **ICPキャニスター内での検証**: `verifyWithEthereum`関数でキャニスター側でも検証 +5. **エラーケース**: 不正なアドレスや改ざんされたメッセージの検証が失敗することを確認 + +## 事前準備 + +テストを実行する前に、以下のステップを順番に実行してください。 + +### 1. 依存関係のインストール + +```bash +cd /application/examples/t_ecdsa +pnpm install +``` + +### 2. ICPローカル環境の起動 + +```bash +cd /application/examples/t_ecdsa +dfx start --clean --background +``` + +### 3. バックエンドキャニスターのビルドとデプロイ + +```bash +cd /application/examples/t_ecdsa +./build.sh +dfx deploy t_ecdsa_backend +dfx generate +``` + +この時点で、`src/declarations/t_ecdsa_backend`配下に型定義ファイルが生成され、`.env`ファイルにキャニスターIDが書き込まれます。 + +## テストの実行 + +### すべてのテストを実行 + +```bash +cd /application/examples/t_ecdsa/src/t_ecdsa_frontend +pnpm test +``` + +### UIモードでテストを実行 + +```bash +pnpm test:ui +``` + +### 一度だけテストを実行(CI用) + +```bash +pnpm test:run +``` + +## テストファイル + +- `testHelper.ts`: ICPキャニスターに接続するためのヘルパー関数 +- `ethereum-sign.test.ts`: Ethereum署名と検証のテストスイート + +## トラブルシューティング + +### キャニスターIDが見つからないエラー + +``` +Error: CANISTER_ID_T_ECDSA_BACKEND is not set +``` + +解決方法: +1. `dfx deploy t_ecdsa_backend`でキャニスターをデプロイ +2. `dfx generate`でTypeScript宣言ファイルと環境変数を生成 + +### Root keyの取得に失敗 + +``` +Unable to fetch root key +``` + +解決方法: +1. `dfx start`でローカルレプリカが起動していることを確認 +2. `http://127.0.0.1:4943`にアクセスできることを確認 + +### タイムアウトエラー + +ICPキャニスターの`signWithEthereum`はECDSA署名のためにthreshold署名を実行するため、時間がかかる場合があります。`vitest.config.ts`の`testTimeout`を増やすことで対応できます。 + +## 参考資料 + +- [viem公式ドキュメント](https://viem.sh/) +- [viem署名関連API](https://viem.sh/docs/actions/wallet/signMessage) +- [viem検証関連API](https://viem.sh/docs/utilities/verifyMessage) +- [DFinity Agent JS](https://github.com/dfinity/agent-js) + diff --git a/examples/t_ecdsa/frontend/src/test/ethereum-sign.test.ts b/examples/t_ecdsa/frontend/src/test/ethereum-sign.test.ts new file mode 100644 index 0000000..02ea283 --- /dev/null +++ b/examples/t_ecdsa/frontend/src/test/ethereum-sign.test.ts @@ -0,0 +1,207 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import { verifyMessage, type Hex, hashMessage, hexToBytes } from 'viem'; +import { createTestActor } from './testHelper'; +import type { _SERVICE } from '../../../declarations/t_ecdsa_backend/t_ecdsa_backend.did'; + +/** + * Ethereum署名のテスト + * + * このテストは以下のフローを実行します: + * 1. ICPキャニスターからEthereumウォレットアドレスを取得 + * 2. メッセージに対してキャニスターで署名を実行 + * 3. viemのverifyMessage関数で署名を検証 + */ +describe('Ethereum署名とviem検証のテスト', () => { + let actor: _SERVICE; + + beforeAll(async () => { + // ICPキャニスターへの接続を確立 + actor = await createTestActor(); + await actor.getNewPublicKey(); + }); + + test('Ethereumアドレスを取得できること', async () => { + const address = await actor.getEvmAddress(); + + // Ethereumアドレスは0xで始まる42文字(0x + 40桁の16進数) + expect(address).toMatch(/^0x[a-fA-F0-9]{40}$/); + + console.log('取得したEthereumアドレス:', address); + }); + + test('signWithEthereum: メッセージをハッシュ化してから署名・検証できること', async () => { + const message = 'Hello, EIP-191!'; + + // Step 1: Ethereumアドレスを取得 + const ethereumAddress = await actor.getEvmAddress(); + expect(ethereumAddress).toMatch(/^0x[a-fA-F0-9]{40}$/); + console.log('Ethereumアドレス:', ethereumAddress); + + // Step 2: メッセージをEIP-191フォーマットでハッシュ化 + // EIP-191: "\x19Ethereum Signed Message:\n" + len(message) + message + const messageHash = hashMessage(message); + expect(messageHash).toMatch(/^0x[a-fA-F0-9]{64}$/); // 32バイトのハッシュ + console.log('メッセージハッシュ (EIP-191):', messageHash); + + // Step 3: ハッシュに対してICPキャニスターで署名を実行 + // signWithEthereumは内部でEIP-191ハッシュ化を行うので、元のメッセージで署名 + const signature = await actor.signWithEthereum(message); + expect(signature).toBeTruthy(); + expect(signature).toMatch(/^0x[a-fA-F0-9]{130}$/); + console.log('署名:', signature); + + // Step 4: viemのverifyMessage関数で署名を検証 + // verifyMessageは内部でEIP-191フォーマットに変換して検証する + const isValid = await verifyMessage({ + address: ethereumAddress as Hex, + message: message, + signature: signature as Hex, + }); + + console.log('署名検証結果:', isValid); + expect(isValid).toBe(true); + }); + + test('signWithEthereum: 複数のメッセージでハッシュ化署名・検証できること', async () => { + const messages = [ + 'EIP-191 test message 1', + 'EIP-191 テストメッセージ 2', + 'EIP-191 test with emoji 🔐', + ]; + + const ethereumAddress = await actor.getEvmAddress(); + + for (const message of messages) { + // メッセージをEIP-191フォーマットでハッシュ化 + const messageHash = hashMessage(message); + console.log(`メッセージ "${message}" のハッシュ:`, messageHash); + + // メッセージに署名(signWithEthereumが内部でEIP-191ハッシュ化を行う) + const signature = await actor.signWithEthereum(message); + + // verifyMessageで検証(内部でEIP-191フォーマットに変換) + const isValid = await verifyMessage({ + address: ethereumAddress as Hex, + message: message, + signature: signature as Hex, + }); + + expect(isValid).toBe(true); + console.log(`メッセージ "${message}" の検証: ${isValid}`); + } + }); + + test('signWithEthereum: 異なるメッセージの署名は検証に失敗すること', async () => { + const message1 = 'Original message'; + const message2 = 'Different message'; + + const ethereumAddress = await actor.getEvmAddress(); + + // message1のハッシュ値を確認 + const hash1 = hashMessage(message1); + const hash2 = hashMessage(message2); + console.log('message1のハッシュ:', hash1); + console.log('message2のハッシュ:', hash2); + + // message1に署名 + const signature = await actor.signWithEthereum(message1); + + // message2で検証を試みる(失敗するはず) + const isValid = await verifyMessage({ + address: ethereumAddress as Hex, + message: message2, + signature: signature as Hex, + }); + + expect(isValid).toBe(false); + console.log('異なるメッセージでの検証結果:', isValid); + }); + + test('signWithEvmWallet: ハッシュ化されたデータに直接署名して検証できること', async () => { + const message = 'Hello, signWithEvmWallet!'; + + // Step 1: Ethereumアドレスを取得 + const ethereumAddress = await actor.getEvmAddress(); + expect(ethereumAddress).toMatch(/^0x[a-fA-F0-9]{40}$/); + console.log('Ethereumアドレス:', ethereumAddress); + + // Step 2: メッセージをEIP-191フォーマットでハッシュ化 + const messageHash = hashMessage(message); + expect(messageHash).toMatch(/^0x[a-fA-F0-9]{64}$/); + console.log('メッセージハッシュ:', messageHash); + + // Step 3: ハッシュをバイト配列に変換してsignWithEvmWalletで署名 + const hashBytes = hexToBytes(messageHash as Hex); + const signature = await actor.signWithEvmWallet(hashBytes); + expect(signature).toBeTruthy(); + expect(signature).toMatch(/^0x[a-fA-F0-9]{130}$/); + console.log('署名:', signature); + + // Step 4: verifyMessageのrawオプションでハッシュを直接検証 + const isValid = await verifyMessage({ + address: ethereumAddress as Hex, + message: message, + signature: signature as Hex, + }); + + console.log('署名検証結果:', isValid); + expect(isValid).toBe(true); + }); + + test('signWithEvmWallet: 複数のメッセージでハッシュ化データの署名・検証', async () => { + const messages = [ + 'signWithEvmWallet test 1', + 'signWithEvmWallet テスト 2', + 'signWithEvmWallet with emoji 🎉', + ]; + + const ethereumAddress = await actor.getEvmAddress(); + + for (const message of messages) { + // メッセージをハッシュ化 + const messageHash = hashMessage(message); + console.log(`メッセージ "${message}" のハッシュ:`, messageHash); + + // ハッシュをバイト配列に変換して署名 + const hashBytes = hexToBytes(messageHash as Hex); + const signature = await actor.signWithEvmWallet(hashBytes); + + // rawオプションで検証 + const isValid = await verifyMessage({ + address: ethereumAddress as Hex, + message: message, + signature: signature as Hex, + }); + + expect(isValid).toBe(true); + console.log(`メッセージ "${message}" の検証: ${isValid}`); + } + }); + + test('signWithEvmWallet: 異なるハッシュでの検証は失敗すること', async () => { + const message1 = 'First message'; + const message2 = 'Second message'; + + const ethereumAddress = await actor.getEvmAddress(); + + // message1のハッシュに署名 + const hash1 = hashMessage(message1); + const hashBytes1 = hexToBytes(hash1 as Hex); + const signature = await actor.signWithEvmWallet(hashBytes1); + + // message2のハッシュで検証を試みる(失敗するはず) + const hash2 = hashMessage(message2); + console.log('署名したハッシュ:', hash1); + console.log('検証に使うハッシュ:', hash2); + + const isValid = await verifyMessage({ + address: ethereumAddress as Hex, + message: message2, + signature: signature as Hex, + }); + + expect(isValid).toBe(false); + console.log('異なるハッシュでの検証結果:', isValid); + }); +}); + diff --git a/examples/t_ecdsa/frontend/src/test/testHelper.ts b/examples/t_ecdsa/frontend/src/test/testHelper.ts new file mode 100644 index 0000000..a13534a --- /dev/null +++ b/examples/t_ecdsa/frontend/src/test/testHelper.ts @@ -0,0 +1,36 @@ +import { HttpAgent } from '@dfinity/agent'; +import { createActor, canisterId } from '../../../declarations/t_ecdsa_backend'; +import type { _SERVICE } from '../../../declarations/t_ecdsa_backend/t_ecdsa_backend.did'; + +/** + * ローカル環境のICPキャニスターに接続するためのヘルパー関数 + */ +export async function createTestActor(): Promise<_SERVICE> { + // 環境変数からキャニスターIDを取得 + // dfx generate実行後に.envファイルに書き込まれる + const effectiveCanisterId = canisterId || process.env.CANISTER_ID_T_ECDSA_BACKEND; + + if (!effectiveCanisterId) { + throw new Error('CANISTER_ID_T_ECDSA_BACKEND is not set. Please run "dfx generate" first.'); + } + + // ローカル環境のホストとポート + const host = 'http://127.0.0.1:4943'; + + // HTTPエージェントを作成 + const agent = await HttpAgent.create({ + host, + }); + + // ローカル環境の場合、fetchRootKeyを実行(本番環境では不要) + await agent.fetchRootKey().catch((err) => { + console.warn('Unable to fetch root key. Check to ensure that your local replica is running'); + throw err; + }); + + // Actorを作成 + const actor = createActor(effectiveCanisterId, { agent }); + + return actor; +} + diff --git a/examples/t_ecdsa/frontend/tsconfig.json b/examples/t_ecdsa/frontend/tsconfig.json new file mode 100644 index 0000000..457c953 --- /dev/null +++ b/examples/t_ecdsa/frontend/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "allowJs": true, + "checkJs": true, + + /* Preact Config */ + "jsx": "react-jsx", + "jsxImportSource": "preact", + "skipLibCheck": true, + "paths": { + "react": ["./node_modules/preact/compat/"], + "react-dom": ["./node_modules/preact/compat/"] + } + }, + "include": ["node_modules/vite/client.d.ts", "**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/examples/t_ecdsa/frontend/vite.config.ts b/examples/t_ecdsa/frontend/vite.config.ts new file mode 100644 index 0000000..8d4a50a --- /dev/null +++ b/examples/t_ecdsa/frontend/vite.config.ts @@ -0,0 +1,71 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'vite'; +import preact from '@preact/preset-vite'; +import { icpBindgen } from '@icp-sdk/bindgen/plugins/vite'; +import { config } from 'dotenv'; + + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +// ルートの .env を読み込む +config({ path: path.resolve(__dirname, '../../.env') }) + +const envVarsToInclude = [ + 'DFX_VERSION', + 'DFX_NETWORK', + 'CANISTER_ID_INTERNET_IDENTITY', + 'CANISTER_ID_T_ECDSA_FRONTEND', + 'CANISTER_ID_T_ECDSA_BACKEND', + 'CANISTER_ID', + 'CANISTER_CANDID_PATH', + 'WALLETCONNECT_PROJECT_ID', +] + +const processEnvObject: Record = {} +for (const key of envVarsToInclude) { + if (process.env[key] !== undefined) { + processEnvObject[key] = process.env[key] as string + } +} + +const injectedProcessEnv = { + NODE_ENV: process.env.NODE_ENV || 'development', + ...processEnvObject, +} + +// https://vitejs.dev/config/ +export default defineConfig({ + server: { + watch: { + usePolling: true, // Docker/リモート環境でホットリロードを確実に + }, + // ICPレプリカへのAPIリクエストをプロキシ(CORS回避・同一オリジン化) + proxy: { + '/api': { + target: 'http://127.0.0.1:4943', + changeOrigin: true, + }, + }, + }, + plugins: [ + preact({ + prerender: { + enabled: true, + renderTarget: '#app', + }, + }), + icpBindgen({ + didFile: "../declarations/t_ecdsa_backend/t_ecdsa_backend.did", + outDir: "./src/bindings", + }), + icpBindgen({ + didFile: "../declarations/internet_identity/internet_identity.did", + outDir: "./src/bindings", + }) + ], + define: { + process: JSON.stringify({ env: injectedProcessEnv }), + 'process.env': JSON.stringify(injectedProcessEnv), + }, +}); diff --git a/examples/t_ecdsa/icp.yaml b/examples/t_ecdsa/icp.yaml index d4f08d0..5cf058e 100644 --- a/examples/t_ecdsa/icp.yaml +++ b/examples/t_ecdsa/icp.yaml @@ -2,6 +2,7 @@ canisters: - backend + - frontend networks: - name: local @@ -9,8 +10,35 @@ networks: gateway: bind: "0.0.0.0" port: 8000 - ii: true + ii: true # http://id.ai.localhost:8000/#authorize environments: - name: local network: local + canisters: + - backend + - frontend + settings: + backend: + environment_variables: + APP_ENV: "local" + + - name: staging + network: ic + canisters: + - backend + - frontend + settings: + backend: + environment_variables: + APP_ENV: "staging" + + - name: production + network: ic + canisters: + - backend + - frontend + settings: + backend: + environment_variables: + APP_ENV: "production" diff --git a/examples/t_ecdsa/t_ecdsa.nimble b/examples/t_ecdsa/t_ecdsa.nimble new file mode 100644 index 0000000..ad6f8d5 --- /dev/null +++ b/examples/t_ecdsa/t_ecdsa.nimble @@ -0,0 +1,14 @@ +# Package + +version = "0.1.0" +author = "Anonymous" +description = "A new awesome nimble package" +license = "MIT" +srcDir = "backend/src" +bin = @["main"] + + +# Dependencies + +requires "nim >= 2.2.10" +requires "https://github.com/dumblepy/nicp_cdk >= 0.1.0" From 0f490c5aeb8e493144e61142079f692191407b45 Mon Sep 17 00:00:00 2001 From: itsumura-h Date: Mon, 24 Aug 2026 06:22:15 +0000 Subject: [PATCH 11/13] fix t_ecdsa to start icp network --- examples/t_ecdsa/frontend/canister.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 examples/t_ecdsa/frontend/canister.yaml diff --git a/examples/t_ecdsa/frontend/canister.yaml b/examples/t_ecdsa/frontend/canister.yaml new file mode 100644 index 0000000..10ad75e --- /dev/null +++ b/examples/t_ecdsa/frontend/canister.yaml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=https://github.com/dfinity/icp-cli/raw/refs/tags/v1.3.0/docs/schemas/canister-yaml-schema.json + +name: frontend + +recipe: + # https://github.com/dfinity/icp-cli-recipes/blob/main/recipes/static-site/README.md + # IMPORTANT: This recipe requires icp-cli >= 0.2.7 + type: "@dfinity/static-site@v0.3.3" + configuration: + build: + # Install the dependencies + # Eventually you might want to use `npm ci` to lock your dependencies + - npm install + - npm run build + dir: dist From 8263829e1e6fb5b3f7deb7745b35f633654bc775 Mon Sep 17 00:00:00 2001 From: itsumura-h Date: Mon, 24 Aug 2026 06:58:39 +0000 Subject: [PATCH 12/13] . --- tests/management_canister/test_ecdsa.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/management_canister/test_ecdsa.nim b/tests/management_canister/test_ecdsa.nim index ce13d23..3470e68 100644 --- a/tests/management_canister/test_ecdsa.nim +++ b/tests/management_canister/test_ecdsa.nim @@ -60,7 +60,7 @@ proc deployExample() = # runCommand(ICP_PATH & " generate internet_identity") # runCommand(ICP_PATH & " deploy t_ecdsa_frontend") # runCommand(ICP_PATH & " generate t_ecdsa_frontend") - runCommand(ICP_PATH & " deploy -y") + runCommand(ICP_PATH & " deploy backend -y") finally: setCurrentDir(originalDir) From d634779c82007ab17e1c6cfa688380a843ef304c Mon Sep 17 00:00:00 2001 From: itsumura-h Date: Mon, 24 Aug 2026 07:15:33 +0000 Subject: [PATCH 13/13] . --- examples/t_ecdsa/backend/backend.did | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/examples/t_ecdsa/backend/backend.did b/examples/t_ecdsa/backend/backend.did index 14c79ac..b27289e 100644 --- a/examples/t_ecdsa/backend/backend.did +++ b/examples/t_ecdsa/backend/backend.did @@ -1,3 +1,22 @@ service : { - greet : (text) -> (text) query; -}; + "getNewPublicKey" : () -> (text); + "getPublicKey" : () -> (text) query; + "signWithEcdsa" : (text) -> (text); + "verifyWithEcdsa": ( + record{ + message:text; + signature:text; + publicKey:text; + } + ) -> (bool); + "getEvmAddress": () -> (text) query; + "signWithEthereum": (mesage:text) -> (text); + "verifyWithEthereum": ( + record{ + message:text; + signature:text; + ethereumAddress:text; + } + ) -> (bool); + "signWithEvmWallet": (blob) -> (text); +}; \ No newline at end of file