diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e7ec2fe --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2024 ICDevs.org - forked from Deland-Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/dfx.json b/dfx.json new file mode 100644 index 0000000..e631ac2 --- /dev/null +++ b/dfx.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "canisters": { + "test": { + "type": "motoko", + "main": "tests/ActorTest.mo", + "args": "-v --compacting-gc" + } + }, + "defaults": { + "build": { + "packtool": "mops sources", + "args": "" + }, + "replica": { + "subnet_type": "system" + } + } + +} diff --git a/mops.toml b/mops.toml new file mode 100644 index 0000000..f6af1dd --- /dev/null +++ b/mops.toml @@ -0,0 +1,13 @@ +[dependencies] +base = "0.11.1" + +[dev-dependencies] +test = "1.2.0" +fuzz = "0.2.1" + +[package] +name = "principal-ext" +version = "0.1.0" +description = "Extensions for Principal" +repository = "https://github.com/icdevsorg/principal-ext.mo" +keywords = [ "prinicipal"] diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..2c9344e --- /dev/null +++ b/readme.md @@ -0,0 +1,52 @@ +This library has been forked from https://github.com/Deland-Labs/fungible-token-standard/blob/06ad30ea8a8c27b6eaada9c214f39e8f50eb8d4b/canisters/dft_motoko/utils/PrincipalExt.mo in order to get it into mops. Please contact austin@icdevs.org if you are Deland-Labs would like take over ownership in mops. + +# Principal Utilities Module + +This project provides a module to work with Internet Computer (IC) Principals, including functions to decode and validate Principal strings, and determine the type of a Principal (canister or user). + +## Overview + +The module provides the following functionalities: +- `fromText`: Decodes a Principal from its textual representation and validates it. Returns null if it fails +- `isCanister`: Checks if a given Principal is a canister ID. +- `isUserPrincipal`: Checks if a given Principal is a user ID. + +## Installation + +```sh +mops add principal-ext +``` + +## Usage + +### fromText + +```motoko + +let principal: ?Principal = PrincipalModule.fromText("aaaaa-aa"); + +``` + +The `fromText` function decodes a Principal from its textual representation and validates it. It returns an optional Principal (`?Principal`). + +### isCanister + +```motoko + +let isCanister: Bool = PrincipalModule.isCanister(principal); +``` + +The `isCanister` function checks if a given Principal is a canister ID. It returns a boolean (`Bool`). + +### isUserPrincipal + +```motoko + +let isUser: Bool = PrincipalModule.isUserPrincipal(principal); +``` + +The `isUserPrincipal` function checks if a given Principal is a user ID. It returns a boolean (`Bool`). + +## License + +This project is licensed under the Apache 2.0 license. \ No newline at end of file diff --git a/src/Base32.mo b/src/Base32.mo new file mode 100644 index 0000000..fe30144 --- /dev/null +++ b/src/Base32.mo @@ -0,0 +1,177 @@ +/// https://github.com/flyq/ic_codec/blob/8a6f2fff88758125fd7e3ce54f99366f9c121eda/src/base32.mo +/// This library lets you encode and decode in either RFC4648 Base32 or in Crockford Base32. + +import Array "mo:base/Array"; +import Iter "mo:base/Iter"; +import Text "mo:base/Text"; +import Char "mo:base/Char"; +import Nat8 "mo:base/Nat8"; +import Int8 "mo:base/Int8"; +import Nat "mo:base/Nat"; +import Nat32 "mo:base/Nat32"; +import Int "mo:base/Int"; + +// refers: https://docs.rs/crate/base32/0.4.0/source/src/lib.rs +module { + let RFC4648_ALPHABET: [Nat8]= [65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 50, 51, 52, 53, 54, 55]; // b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" + let CROCKFORD_ALPHABET: [Nat8] = [48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 83, 84, 86, 87, 88, 89, 90]; // b"0123456789ABCDEFGHJKMNPQRSTVWXYZ" + let RFC4648_INV_ALPHABET: [Int8] = [-1, -1, 26, 27, 28, 29, 30, 31, -1, -1, -1, -1, -1, 0, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]; + let CROCKFORD_INV_ALPHABET: [Int8] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, 16, 17, 1, 18, 19, 1, 20, 21, 0, 22, 23, 24, 25, 26, -1, 27, 28, 29, 30, 31]; + + /// RFC4648 Base32 or Crockford Base32 + public type Alphabet = { + #RFC4648: { padding: Bool; }; + #Crockford; + }; + + /// encode the bytes + public func encode(alphabet: Alphabet, data: [Nat8]) : Text { + let (alpha, padding) = switch alphabet { + case (#RFC4648 { padding }) { (RFC4648_ALPHABET, padding); }; + case (#Crockford) { (CROCKFORD_ALPHABET, false); }; + }; + let len =(data.size() + 3)/4*5; + var ret: [var Nat8] = [var]; + var res: Text = ""; + let chunks = bytesToChunks(data, 5); + for (i in chunks.keys()) { + let buf: [var Nat8] = Array.init(5, 0); + for (j in chunks[i].keys()) { + buf[j] := chunks[i][j]; + }; + ret := Array.thaw(Array.append(Array.freeze(ret), [alpha[Nat8.toNat((buf[0] & 0xF8) >> 3)]])); + ret := Array.thaw(Array.append(Array.freeze(ret), [alpha[Nat8.toNat(((buf[0] & 0x07) << 2) | ((buf[1] & 0xC0) >> 6))]])); + ret := Array.thaw(Array.append(Array.freeze(ret), [alpha[Nat8.toNat((buf[1] & 0x3E) >> 1)]])); + ret := Array.thaw(Array.append(Array.freeze(ret), [alpha[Nat8.toNat(((buf[1] & 0x01) << 4) | ((buf[2] & 0xF0) >> 4))]])); + ret := Array.thaw(Array.append(Array.freeze(ret), [alpha[Nat8.toNat(((buf[2] & 0x0F) << 1) | (buf[3] >> 7))]])); + ret := Array.thaw(Array.append(Array.freeze(ret), [alpha[Nat8.toNat((buf[3] & 0x7C) >> 2)]])); + ret := Array.thaw(Array.append(Array.freeze(ret), [alpha[Nat8.toNat(((buf[3] & 0x03) << 3) | ((buf[4] & 0xE0) >> 5))]])); + ret := Array.thaw(Array.append(Array.freeze(ret), [alpha[Nat8.toNat(buf[4] & 0x1F)]])); + }; + var len_ret: Nat = ret.size(); + if ((data.size() % 5) != 0) { + let len = ret.size(); + var num_extra = 0; + if (8 < ((data.size() % 5 * 8 + 4) / 5)) { + num_extra := 0; + } else { + num_extra := 8 - ((data.size() % 5 * 8 + 4) / 5); + }; + if padding { + for (i in Iter.range(1, num_extra)) { + ret[len - i] := 61; // b'=' == 61 + }; + } else { + len_ret := len - num_extra; + }; + }; + for (i in Iter.range(0, len_ret-1)) { + res := res # Char.toText(Char.fromNat32(Nat32.fromNat(Nat8.toNat(ret[i]) ))); + }; + return res; + }; + + /// decode the text + public func decode(alphabet: Alphabet, data: Text) : ?[Nat8] { + if (not is_ascii(data)) { + return null; + }; + var bytes: [Nat8] = []; + for (i in Text.toIter(data)) { + bytes := Array.append(bytes, [Nat8.fromNat(Nat32.toNat(Char.toNat32(i)))]); + }; + let alpha = switch (alphabet) { + case (#RFC4648 { padding }) { RFC4648_INV_ALPHABET; }; + case (#Crockford) { CROCKFORD_INV_ALPHABET; }; + }; + var unpadded_bytes_length = bytes.size(); + label l for (i in Iter.range(1, Nat.min(6, bytes.size()))) { + if (bytes[bytes.size() - i] != 61) { // b'=' == 61 + break l; + }; + unpadded_bytes_length -= 1; + }; + let output_length = unpadded_bytes_length*5/8; + var ret: [Nat8] = []; + let ret_len = (output_length+4)/5*5; + let chunks = bytesToChunks(bytes, 8); + for (i in chunks.keys()) { + let buf: [var Nat8] = Array.init(8, 0); + for (j in chunks[i].keys()) { + switch (get_element(alpha, Nat8.toNat(wrapping_sub(to_ascii_uppercase(chunks[i][j]), 48)) )) { // b'0' == 48 + case (?-1 or null) { return null; }; + case (?val) { buf[j] := Int8.toNat8(val); }; + } + }; + ret := Array.append(ret, [((buf[0] << 3) | (buf[1] >> 2))]); + ret := Array.append(ret, [((buf[1] << 6) | (buf[2] << 1) | (buf[3] >> 4))]); + ret := Array.append(ret, [((buf[3] << 4) | (buf[4] >> 1))]); + ret := Array.append(ret, [((buf[4] << 7) | (buf[5] << 2)) | (buf[6] >> 3)]); + ret := Array.append(ret, [((buf[6] << 5) | buf[7])]); + }; + var res = Array.init(output_length, 0); + for (i in res.keys()) { + res[i] := ret[i]; + }; + return ?Array.freeze(res); + }; + + func bytesToChunks(bytes: [Nat8], interval: Nat) : [[Nat8]] { + let len = bytes.size(); + var ret: [[Nat8]] = []; + for (i in Iter.range(1, len)) { + var chunk: [var Nat8] = Array.init(interval, 0); + if (i % interval == 0) { + for (j in Iter.range(0, interval-1)) { + chunk[j] := bytes[i-(interval-j)]; + }; + ret := Array.append(ret, [Array.freeze(chunk)]); + }; + }; + if (len % interval != 0) { + var chunk: [Nat8] = []; + for (i in Iter.range(0, (len % interval) - 1)) { + chunk := Array.append(chunk, [bytes[len - (len % interval) + i]]); + }; + ret := Array.append<[Nat8]>(ret, [chunk]); + }; + return ret; + }; + + func is_ascii(a: Text) : Bool { + for (i in Text.toIter(a)) { + if (Char.toNat32(i) > 0x7F) { + return false; + }; + }; + return true; + }; + + func to_ascii_uppercase(a: Nat8) : Nat8 { + if (is_ascii_lowercase(Char.fromNat32(Nat32.fromNat(Nat8.toNat(a))))) { + return 32 ^ a; + } else { + return a; + }; + }; + + func is_ascii_lowercase(a: Char) : Bool { + return (a >= 'a' and a <= 'z'); + }; + + func get_element(a: [Int8], index: Nat) : ?Int8 { + if (index < a.size()) { + return ?a[index]; + } else { + return null; + }; + }; + + func wrapping_sub(a: Nat8, b: Nat8) : Nat8 { + if (a < b) { + return 255 - b + a + 1; + } else { + return a - b; + }; + }; +}; \ No newline at end of file diff --git a/src/Crc32.mo b/src/Crc32.mo new file mode 100644 index 0000000..620bbc3 --- /dev/null +++ b/src/Crc32.mo @@ -0,0 +1,70 @@ +/** +CRC32.mo based on: https://github.com/enzoh/motoko-crc/blob/master/src/CRC8.mo + */ + +import Prim "mo:prim"; +import Nat "mo:base/Nat"; +import Nat8 "mo:base/Nat8"; +import Nat32 "mo:base/Nat32"; + +module { + private let tablecrc32 : [Nat32] = [ + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, + 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, + 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, + 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, + 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, + 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, + 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, + 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, + 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, + 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, + 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106, + 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, + 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, + 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, + 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, + 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, + 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, + 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, + 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, + 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, + 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, + 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, + 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, + 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, + 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, + 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, + 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, + 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, + 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, + 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, + 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, + 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, + 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, + 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, + 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, + 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, + 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, + 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, + 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, + 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, + 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, + 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, + 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d + ]; + + public func crc32(data : [Nat8]) : [Nat8] { + var crc : Nat32 = 4294967295; + for (byte in data.vals()) { + crc := (crc >> 8) ^ tablecrc32[Nat32.toNat((crc ^ Nat32.fromNat(Nat8.toNat(byte))) & 0xFF)]; + }; + crc := (crc ^ 0xFFFFFFFF) & 0xffffffff; + return [ + Nat8.fromNat(Nat32.toNat((crc >> 24) & 0xFF)), + Nat8.fromNat(Nat32.toNat((crc >> 16) & 0xFF)), + Nat8.fromNat(Nat32.toNat((crc >> 8) & 0xFF)), + Nat8.fromNat(Nat32.toNat((crc) & 0xFF)) + ]; + }; +}; \ No newline at end of file diff --git a/src/lib.mo b/src/lib.mo new file mode 100644 index 0000000..e42f1c5 --- /dev/null +++ b/src/lib.mo @@ -0,0 +1,68 @@ +import Text "mo:base/Text"; +import Base32 "Base32"; +import CRC32 "Crc32"; +import Principal "mo:base/Principal"; +import List "mo:base/List"; +import Array "mo:base/Array"; +import Blob "mo:base/Blob"; +import Prim "mo:⛔"; + +module { + let CRC_LENGTH_IN_BYTES: Nat = 4; + let CANISTER_ID_HASH_LEN_IN_BYTES: Nat = 10; + let HASH_LEN_IN_BYTES: Nat = 28; + let MAX_LENGTH_IN_BYTES: Nat = 29; //HASH_LEN_IN_BYTES + 1; // 29 + let TYPE_SELF_AUTH: Nat8 = 0x02; + + public func fromText(text : Text) : ?Principal { + var _text = Text.map(text , Prim.charToLower); + _text := Text.replace(_text , #text "-" , ""); + let decodeResult = Base32.decode(#RFC4648({ padding=false; }),_text); + let bytes:[Nat8] = switch (decodeResult) + { + case null []; + case (?b) b; + }; + + let bytesSize = bytes.size(); + + if ( bytes.size() < CRC_LENGTH_IN_BYTES ) { return null; } + else if ( bytes.size() > MAX_LENGTH_IN_BYTES + CRC_LENGTH_IN_BYTES) { return null; } + else if ( text == "aaaaa-aa") { return ?Principal.fromText(text); } + else { + let body = Array.init(bytesSize - 4, 0) ; + + for (k in bytes.keys()) { + if ( k > 3 ) { + body[ k - 4 ] := bytes [ k ]; + } + }; + + let crcResult : [Nat8] = CRC32.crc32(Array.freeze(body)); + + for (c in crcResult.keys()){ + if ( bytes[c] != crcResult[c]) { + return null; + } + }; + + return ?Principal.fromText(text); + }; + }; + + public func isCanister(id: Principal) : Bool { + let bytes = List.fromArray(Blob.toArray(Principal.toBlob(id))); + List.size(bytes) == CANISTER_ID_HASH_LEN_IN_BYTES + }; + + public func isUserPrincipal(id: Principal) : Bool { + let bytes = List.fromArray(Blob.toArray(Principal.toBlob(id))); + if (List.size(bytes) != HASH_LEN_IN_BYTES + 1) { + return false; + }; + if (List.last(bytes) != ?TYPE_SELF_AUTH) { + return false; + }; + true + }; +}; \ No newline at end of file diff --git a/tests/fromText.test.mo b/tests/fromText.test.mo new file mode 100644 index 0000000..61c5307 --- /dev/null +++ b/tests/fromText.test.mo @@ -0,0 +1,13 @@ +import {test} "mo:test"; +import Principal "mo:base/Principal"; +import ext "../src/"; +import FuzzBlob "mo:fuzz/Blob"; +import Fuzz "mo:fuzz"; + +test("fromText", func(){ + assert (ext.fromText("aaaaa-aa") == ?Principal.fromText("aaaaa-aa")); + assert (ext.fromText("gobolygoop") == null); + assert (ext.fromText("qoctq-giaaa-aaaaa-aaaea-cai") == ?Principal.fromText("qoctq-giaaa-aaaaa-aaaea-cai")); + assert (ext.fromText("7nqmm-3byi2-wmrec-ccm5q-h6pcs-qxnxj-6jtmo-ytrbv-nlw4b-dkmbx-rqe") == ?Principal.fromText("7nqmm-3byi2-wmrec-ccm5q-h6pcs-qxnxj-6jtmo-ytrbv-nlw4b-dkmbx-rqe")); + +}); \ No newline at end of file