diff --git a/.cursor/rules/branch/118-icstablebtree.mdc b/.cursor/rules/branch/118-icstablebtree.mdc new file mode 100644 index 0000000..f08b81f --- /dev/null +++ b/.cursor/rules/branch/118-icstablebtree.mdc @@ -0,0 +1,648 @@ +# 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 のままとする。 +- [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` は削除した。 +- [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 は可能な限り維持する。 + +## 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/.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/README.md b/README.md index 2ba25fb..6d79e47 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,7 @@ Store key-value pairs that persist across canister upgrades. ```nim import nicp_cdk/storage/stable_table -# Create a stable table mapping strings to integers +# Create a stable B+Tree mapping strings to integers var scoreTable = initIcStableTable[string, uint]() # Store a key-value pair @@ -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 @@ -230,9 +230,42 @@ 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 table: +You can also store custom Nim objects in a stable B+Tree: ```nim import nicp_cdk @@ -243,7 +276,7 @@ type UserProfile = object name: string active: bool -# Create a stable table mapping principals to user profiles +# Create a stable B+Tree mapping principals to user profiles var userTable = initIcStableTable[Principal, UserProfile]() # Store a user profile diff --git a/benchmarks/storage/stable_exact_lookup.nim b/benchmarks/storage/stable_exact_lookup.nim new file mode 100644 index 0000000..f1c8e5c --- /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/libs/memory_view +import ../../src/nicp_cdk/storage/stable_table +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 = 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 + 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/docs/en/stable_memory.md b/docs/en/stable_memory.md index f18149e..033871b 100644 --- a/docs/en/stable_memory.md +++ b/docs/en/stable_memory.md @@ -11,10 +11,10 @@ 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 +- `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 @@ -75,33 +75,37 @@ 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 -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/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 5806db8..db6d0cf 100644 --- a/examples/stable_memory/backend/backend.did +++ b/examples/stable_memory/backend/backend.did @@ -24,15 +24,15 @@ service : { "seqInt_setAt": (nat, int) -> (); "seqInt_delete": (nat) -> (); "seqInt_values": () -> (vec int) 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) -> (); - "table_get": () -> (text) query; + "table_set": (text, text) -> (); + "table_get": (text) -> (text) query; + "table_hasKey": (text) -> (bool) 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; + "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 5de2a0e..b81407b 100644 --- a/examples/stable_memory/backend/src/main.nim +++ b/examples/stable_memory/backend/src/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_hash_map # Define base offsets for each storage structure to avoid collision const @@ -16,8 +16,10 @@ const CharDbOffset = 700000'u64 ByteDbOffset = 800000'u64 SeqIntDbOffset = 900000'u64 - TableDbOffset = 2000000'u64 - ObjectDbOffset = 3000000'u64 + BTreeDbOffset = 2000000'u64 + BTreeDbLimit = 1000000'u64 + HashDbOffset = 3000000'u64 + HashDbLimit = 1000000'u64 # ================================================== # int @@ -202,82 +204,87 @@ proc seqInt_values() {.query.} = reply(seqIntDb.toSeq()) # ================================================== -# Table[principal, string] +# IcStableHashMap[string, string] # ================================================== -var tableDb = initIcStableTable[Principal, string](TableDbOffset) +var hashDb = initIcStableHashMap[string, string]( + initRawMemoryView(HashDbOffset, HashDbLimit) +) -proc table_reset() {.update.} = - tableDb.clear() +proc hash_reset() {.update.} = + hashDb.clear() reply() -proc table_set() {.update.} = - let principal = Msg.caller() +proc hash_set() {.update.} = let request = Request.new() - let message = request.getStr(0) - tableDb[principal] = message + hashDb[request.getStr(0)] = request.getStr(1) 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.} = +proc hash_get() {.query.} = let request = Request.new() - let principal = request.getPrincipal(0) - let message = request.getStr(1) - tableDb[principal] = message - reply() + reply(hashDb[request.getStr(0)]) -proc table_getFor() {.query.} = +proc hash_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(hashDb.hasKey(request.getStr(0))) -proc table_values() {.query.} = - var values: seq[string] = @[] - for value in tableDb.values(): - values.add(value) - reply(values) +proc hash_len() {.query.} = + reply(uint(hashDb.len())) # ================================================== -# object +# IcStableTable[string, string] (B+Tree implementation) # ================================================== -type UserProfile = object - id: uint - name: string - active: bool +# This map keeps its searchable index in stable memory, while `range` shows +# the key-order traversal provided by the B+Tree backend. +type TableEntry = object + key: string + value: string + +var tableDb = initIcStableTable[string, string]( + initRawMemoryView(BTreeDbOffset, BTreeDbLimit) +) -var objectDb = initIcStableTable[Principal, UserProfile](ObjectDbOffset) +proc table_reset() {.update.} = + tableDb.clear() + reply() -proc object_set() {.update.} = +proc table_set() {.update.} = try: - let principal = Msg.caller() + icEcho("table_set: begin") 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) + icEcho("table_set: request decoded") + let key = request.getStr(0) + let value = request.getStr(1) + icEcho("table_set: writing key=", key) + tableDb[key] = value + icEcho("table_set: write complete") reply() + icEcho("table_set: reply sent") except Exception as e: - echo "Error: ", e.msg - reply(e.msg) + ## 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("table_set failed: ", e.msg) + raise -proc object_get() {.query.} = - let principal = Msg.caller() - let value = objectDb[principal] - reply(value) +proc table_get() {.query.} = + let request = Request.new() + let key = request.getStr(0) + reply(tableDb[key]) + +proc table_hasKey() {.query.} = + let request = Request.new() + reply(tableDb.hasKey(request.getStr(0))) + +proc table_len() {.query.} = + reply(uint(tableDb.len())) + +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[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/main.nim b/examples/stable_memory/src/stable_memory_backend/main.nim deleted file mode 100644 index 5de2a0e..0000000 --- a/examples/stable_memory/src/stable_memory_backend/main.nim +++ /dev/null @@ -1,283 +0,0 @@ -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 - -# 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 - TableDbOffset = 2000000'u64 - ObjectDbOffset = 3000000'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()) - -# ================================================== -# Table[principal, string] -# ================================================== -var tableDb = initIcStableTable[Principal, string](TableDbOffset) - -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) - -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) diff --git a/examples/stable_memory/stable_memory.did b/examples/stable_memory/stable_memory.did deleted file mode 100644 index 5806db8..0000000 --- a/examples/stable_memory/stable_memory.did +++ /dev/null @@ -1,38 +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; - "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; -}; diff --git a/examples/stable_memory/test_clean.sh b/examples/stable_memory/test_clean.sh deleted file mode 100755 index f5ee99a..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 table_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 '()' - -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 table_setFor "(principal \"aaaaa-aa\", \"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 "table_getFor(aaaaa-aa):" -$DFX canister call stable_memory_backend table_getFor "(principal \"aaaaa-aa\")" - -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 "table_getFor(aaaaa-aa):" -$DFX canister call stable_memory_backend table_getFor "(principal \"aaaaa-aa\")" 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_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/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/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..b27289e --- /dev/null +++ b/examples/t_ecdsa/backend/backend.did @@ -0,0 +1,22 @@ +service : { + "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 diff --git a/examples/stable_memory/src/stable_memory_backend/config.nims b/examples/t_ecdsa/backend/config.nims similarity index 58% rename from examples/stable_memory/src/stable_memory_backend/config.nims rename to examples/t_ecdsa/backend/config.nims index 55b4688..0a2f792 100644 --- a/examples/stable_memory/src/stable_memory_backend/config.nims +++ b/examples/t_ecdsa/backend/config.nims @@ -8,37 +8,37 @@ import std/os --cc: "clang" --define: "useMalloc" -# Enforce static linking for the WASI target to make it self-contained, similar to icpp-pro +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") # 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 +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") -# 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 + switch("passC", "-Os") + switch("passC", "-flto") + switch("passL", "-flto") -# 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/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/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 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" diff --git a/runTest.sh b/runTest.sh index 3a9e0e6..637740e 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() { @@ -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/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/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/libs/linear_hashing.nim b/src/nicp_cdk/storage/libs/linear_hashing.nim new file mode 100644 index 0000000..cde0b06 --- /dev/null +++ b/src/nicp_cdk/storage/libs/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/libs/memory_manager.nim b/src/nicp_cdk/storage/libs/memory_manager.nim new file mode 100644 index 0000000..0808497 --- /dev/null +++ b/src/nicp_cdk/storage/libs/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/libs/memory_view.nim b/src/nicp_cdk/storage/libs/memory_view.nim new file mode 100644 index 0000000..70ad19b --- /dev/null +++ b/src/nicp_cdk/storage/libs/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/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/libs/stable_allocator.nim b/src/nicp_cdk/storage/libs/stable_allocator.nim new file mode 100644 index 0000000..1577948 --- /dev/null +++ b/src/nicp_cdk/storage/libs/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/libs/stable_btree.nim b/src/nicp_cdk/storage/libs/stable_btree.nim new file mode 100644 index 0000000..1492fd5 --- /dev/null +++ b/src/nicp_cdk/storage/libs/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.} + IcStableTable*[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: IcStableTable): int = (int(t.nodeSize) - NodeHeaderSize) div SlotSize + +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. + 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: 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: 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) + 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 IcStableTable[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: 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] + 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: 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) + 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 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: 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: 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 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 + 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 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: 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: 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: 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 + 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 IcStableTable[K, V], kind: uint8): uint64 = + result = t.alloc(uint64(t.nodeSize), uint64(t.nodeSize)); t.writeNode(result, Node(kind: kind)) + +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 + 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 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 = initIcStableTable[K, V](memory, codec, cacheSlots, valueCodecId) + result.builtinCodec = true + +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) + 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: 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) + 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: 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 + 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 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): + 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 IcStableTable[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: IcStableTable[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: IcStableTable[K, V]): K = + for key, _ in t.pairs: yield key +iterator values*[K, V](t: IcStableTable[K, V]): V = + for _, value in t.pairs: yield value + +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) + 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 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. + t.nodeSize = DefaultNodeSize + t.header = BTreeHeader(arenaEnd: SuperblockSize) + t.writeHeader diff --git a/src/nicp_cdk/storage/libs/stable_hash.nim b/src/nicp_cdk/storage/libs/stable_hash.nim new file mode 100644 index 0000000..90e30b2 --- /dev/null +++ b/src/nicp_cdk/storage/libs/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/libs/stable_key_codec.nim b/src/nicp_cdk/storage/libs/stable_key_codec.nim new file mode 100644 index 0000000..a6d3df2 --- /dev/null +++ b/src/nicp_cdk/storage/libs/stable_key_codec.nim @@ -0,0 +1,98 @@ +## Ordered, versioned encodings for keys used by IcStableTable. + +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: "IcStableTable 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: "IcStableTable keys require a StableKeyCodec-supported type".} 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 new file mode 100644 index 0000000..df42df5 --- /dev/null +++ b/src/nicp_cdk/storage/stable_hash_map.nim @@ -0,0 +1,301 @@ +## 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 ./libs/serialization +import ./libs/memory_view +import ./libs/linear_hashing +import ./libs/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 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) + 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]) = + if t.maxBucketLoad == 0: + t.maxBucketLoad = DefaultBucketLoad + 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) = + 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: + 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_seq.nim b/src/nicp_cdk/storage/stable_seq.nim index 6748ea5..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 ./serialization -import ./stable_memory +import ./libs/serialization +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/src/nicp_cdk/storage/stable_table.nim b/src/nicp_cdk/storage/stable_table.nim index d0e7e64..d22362b 100644 --- a/src/nicp_cdk/storage/stable_table.nim +++ b/src/nicp_cdk/storage/stable_table.nim @@ -1,177 +1,7 @@ -import std/endians -import std/tables +## Public stable key-value table API backed by a stable-memory B+Tree. -import ./serialization -import ./stable_memory +import ./libs/stable_btree +import ./libs/memory_view -const - TableMagic = [byte('S'), byte('T'), byte('B'), byte('L')] - TableVersion = 1'u32 - TableHeaderSize = 32'u64 - -type EntryInfo = object - offset: uint64 - keyLen: uint32 - valueLen: uint32 - -type IcStableTable*[K, V] = object - baseOffset: uint64 - count: uint64 - dataEnd: uint64 - index: Table[string, EntryInfo] - -proc bytesToString(data: openArray[byte]): string = - result = newString(data.len) - if data.len > 0: - copyMem(addr result[0], unsafeAddr data[0], data.len) - -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) +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/management_canister/test_ecdsa.nim b/tests/management_canister/test_ecdsa.nim index 06b4945..3470e68 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 @@ -57,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) diff --git a/tests/storage/test_linear_hashing.nim b/tests/storage/test_linear_hashing.nim new file mode 100644 index 0000000..c25a83b --- /dev/null +++ b/tests/storage/test_linear_hashing.nim @@ -0,0 +1,29 @@ +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/libs/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..3340423 --- /dev/null +++ b/tests/storage/test_memory_manager_api.nim @@ -0,0 +1,18 @@ +discard """ + cmd: "nim c --skipUserCfg $file" +""" + +# nim c -r --skipUserCfg tests/storage/test_memory_manager_api.nim + +import ../../src/nicp_cdk/storage/libs/memory_manager +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_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 new file mode 100644 index 0000000..62ec22e --- /dev/null +++ b/tests/storage/test_stable_btree.nim @@ -0,0 +1,105 @@ +discard """ + cmd: "nim c -r -d:nicpMemoryViewOnly --skipUserCfg $file" +""" + +import std/unittest +import std/options +import std/tables +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] +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 + ) + +suite "stable B+Tree": + test "split, ordered iteration, update, and reopen": + let backing = InMemoryStable(data: @[]) + 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: + 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 = 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 = initIcStableTable[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 "custom key codec is persisted and used for ordering": + let backing = InMemoryStable(data: @[]) + let codec = compositeCodec() + 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 = 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 = initIcStableTable[uint32, string](backing.memoryView(), valueCodecId = 1) + tree[1'u32] = "one" + expect ValueError: + 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 new file mode 100644 index 0000000..6296ed2 --- /dev/null +++ b/tests/storage/test_stable_btree_api.nim @@ -0,0 +1,30 @@ +discard """ + 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_table + +proc apiShape() = + var table = initIcStableTable[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 = initIcStableTable[uint32, string](cacheSlots = 0) + uncached[1'u32] = "one" + discard uncached[1'u32] + +static: + doAssert compiles(apiShape()) diff --git a/tests/storage/test_stable_hash.nim b/tests/storage/test_stable_hash.nim new file mode 100644 index 0000000..f7a4bb0 --- /dev/null +++ b/tests/storage/test_stable_hash.nim @@ -0,0 +1,20 @@ +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/libs/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..b6892e3 --- /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/libs/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..567d3a3 --- /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/libs/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/storage/test_stable_memory.nim b/tests/storage/test_stable_memory.nim index ed21af3..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,6 +99,7 @@ proc resetAllDatabases() = discard callCanisterFunction("char_set", "(0)") discard callCanisterFunction("byte_set", "(0)") discard callCanisterFunction("seqInt_reset") + discard callCanisterFunction("hash_reset") discard callCanisterFunction("table_reset") suite "stable memory backend tests": @@ -166,67 +181,71 @@ 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": + 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("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_hasKey", "(\"one\")") == "(false)" + + discard callCanisterFunction("table_set", "(\"two\", \"second\")") + discard callCanisterFunction("table_set", "(\"one\", \"first\")") 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_get", "(\"one\")") == "(\"first\")" + + ## Updating an existing key must not change the live-entry count. + discard callCanisterFunction("table_set", "(\"one\", \"updated\")") 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 "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:") + check callCanisterFunction("table_get", "(\"one\")") == "(\"updated\")" + check callCanisterFunction("table_hasKey", "(\"two\")") == "(true)" + + let ranged = callCanisterFunction("table_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 "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("table_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("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("table_getFor", "(principal \"aaaaa-aa\")") + 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()) 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)