Internet Computer HTTPS Interface
The Internet Computer HTTPS API accepts a CBOR-encoded data structure via POST to a URL such as https://ic0.app/api/v2/canister/fmfd2-7qaaa-aaaae-aaapq-cai/call
We show how to craft the CBOR object described in the spec and send it to a given canister. For a challenge, we avoid libraries while keeping our source short enough to fit comfortably on this page.
Encoding to CBOR
The following Haskell compiles to a wasm binary that produces a CBOR blob with a suitable expiry timestamp given a canister, request type, method name, and argument:
module Main where import Base foreign import ccall "argc" argc :: IO Int foreign import ccall "argvlen" argvlen :: Int -> IO Int foreign import ccall "argvat" argvat :: Int -> Int -> IO Char foreign import ccall "putchar" putChar :: Char -> IO () foreign import ccall "now" now :: IO Int byte n = (toEnum n:) cborN t n | n < 24 = hdr $ fromIntegral n | n < 2^8 = hdr 24 . byte (fromIntegral n) | n < 2^16 = hdr 25 . cborLong 2 n | n < 2^32 = hdr 26 . cborLong 4 n | otherwise = hdr 27 . cborLong 8 n where hdr k = byte $ t*32 + k cborLong 0 _ = id cborLong k n = cborLong (k - 1) q . byte (fromIntegral r) where (q, r) = divMod n 256 cborBlob bs = cborN 2 (length bs) . foldr (.) id (byte <$> bs) cborTxt s = cborN 3 (length s) . (s++) cborMap kvs = cborN 5 (length kvs) . foldr (.) id (uncurry (.) <$> kvs) enc :: [Int] -> String -> String -> [Int] -> Integer -> String -> String enc canID request_type method arg t = cborMap [(cborTxt "content", cborMap [ (cborTxt "ingress_expiry", cborN 0 t) , (cborTxt "sender" , cborBlob [0x04]) , (cborTxt "canister_id" , cborBlob canID) , (cborTxt "request_type" , cborTxt request_type) , (cborTxt "method_name" , cborTxt method) , (cborTxt "arg" , cborBlob arg) ] )] unbase32 raw = go 0 0 s where go acc len rest | 8 <= len = fromIntegral q : go r (len - 8) rest | otherwise = case rest of  ->  n:ns -> go (acc*32 + n) (len + 5) ns where (q, r) = divMod acc (2^(len - 8)) tab = zip (['a'..'z'] ++ ['2'..'7']) [0..] s = concatMap (maybe  pure . (`lookup` tab)) raw getArgs = do count <- argc forM [0..count - 1] \n -> do len <- argvlen n forM [0..len - 1] $ argvat n foreign export ccall "go" main main = do [can, request_type, method, arg] <- getArgs t <- fromIntegral <$> now let buf = enc (drop 4 $ unbase32 can) request_type method (fromEnum <$> arg) (10^9 * (t + 120)) "" mapM_ putChar buf
sender: Our demo uses the anonymous sender, which is represented with the fixed ID 4.
canister_id: The raw canister ID can be computed by base32-decoding the ID then dropping the initial 4-byte CRC checksum. In the shell:
echo fmfd2-7qaaa-aaaae-aaapq-cai= | tr a-z A-Z | base32 -d -i | tail -c+5 | xxd
request_type: This must agree with the end of the URL path, which is call in the example above. This seems redundant, but perhaps there is a good reason for also specifying the method type in the URL.
Sending a POST
Clicking on the "Send!" button triggers the go function, which calls agent to send a request to the Internet Computer. A successful call yields a plain 202 response. A query yields a 200 with a blob containing a CBOR-encoded map. Within this map, the status field indicates whether the query was rejected or accepted.
We assume status 200 queries are successful and a fragile ad hoc CBOR decoder does just enough work to extract the reply, which is typically a Candid message.