Just in case it helps, this is my current attempt to create a stream (which seems to work), retrieve data from it (also seems to work) and update it (here’s where the structure building and signing is problematic):
import requests
import jsonpatch
from jose import jws, jwt
from base64 import urlsafe_b64encode, urlsafe_b64decode
import numpy as np
import dag_cbor
from cid import make_cid
import hashlib
import json
import sys
from ipfs import IPFSHashOnly
# Glaze quick start
# https://developers.ceramic.network/build/cli/quick-start/#__tabbed_1_1
# ceramic daemon
# glaze did:create -> Created DID did:key:z6MkfdMoFrvCJgQ5EyAzTs6es1zpRiBTU5iK78T2ixJens7z with seed 2172441f1cc497e9aa3d9841f6b39f28f5b9ce216f96f7f5e03f5f7a8d481884
# glaze tile:create --key 2172441f1cc497e9aa3d9841f6b39f28f5b9ce216f96f7f5e03f5f7a8d481884 --content '{"Foo":"Bar"}'
# glaze tile:update kjzl6cwe1jw148hj4mdddncgjemxhuexbaea1vlmlecuvuxdllmacbgumu9vard --key 2172441f1cc497e9aa3d9841f6b39f28f5b9ce216f96f7f5e03f5f7a8d481884 --content '{"Foo":"Baz"}'
# glaze tile:show kjzl6cwe1jw148hj4mdddncgjemxhuexbaea1vlmlecuvuxdllmacbgumu9vard
class CeramicManager:
# Docs:
# https://developers.ceramic.network/build/http/api/#__tabbed_3_1
# url_base = "https://gateway-clay.ceramic.network" # gateway node (read only)
url_base = "https://ceramic-clay.3boxlabs.com" # read & write
create_stream_endpoint = ""
def __init__(self) -> None:
pass
def make_request(self, url: str, request_type: str="get", json_data: dict={}):
if request_type not in ("get", "post", "delete"):
return None, None
if request_type == "get":
response = requests.get(url)
if request_type == "post":
response = requests.post(url, json=json_data)
if request_type == "delete":
response = requests.delete(url)
return response.status_code, response.json() if "application/json" in response.headers.get("content-type") else {}
def create_stream(self, genesis: dict, options: dict={}, type: int=0):
return self.make_request(f"{self.url_base}/api/v0/streams", "post", json_data=dict(genesis=genesis, opts=options, type=type))
def get_stream(self, streamid: str):
return self.make_request(f"{self.url_base}/api/v0/streams/{streamid}", "get")
def pin_stream(self, streamid: str):
return self.make_request(f"{self.url_base}/api/v0/pins/{streamid}", "post")
def unpin_stream(self, streamid: str):
return self.make_request(f"{self.url_base}/api/v0/pins/{streamid}", "delete")
def get_commits(self, streamid: str):
return self.make_request(f"{self.url_base}/api/v0/commits/{streamid}", "get")
def add_commit(self, streamid: str, commit: dict, options: dict={}):
return self.make_request(f"{self.url_base}/api/v0/commits", "post", json_data=dict(streamId=streamid, commit=commit, opts=options))
# Generate a key:did (for simplicity) with Glaze. We do not need encryption.
did = "did:key:z6MkfdMoFrvCJgQ5EyAzTs6es1zpRiBTU5iK78T2ixJens7z"
did_seed = "172441f1cc497e9aa3d9841f6b39f28f5b9ce216f96f7f5e03f5f7a8d481884"
# Instantiate the manager
cman = CeramicManager()
# Prepare the genesis commit: no need to sign if there's no data on it
genesis_data = {
"header": {
"family": "test",
"controllers": [did], # who is allowed to make updates
},
}
# Create a new stream
code, data = cman.create_stream(genesis=genesis_data)
print("Create stream", code)
print(data)
streamid = data["streamId"]
genesis_cid = data["state"]["log"][0]["cid"]
# Get a stream
# cman.get_stream(streamid)
# Get the latest commit
code, data = cman.get_commits(streamid)
print("Get commits", code)
print(data)
latest_cid = data["commits"][-1]["cid"]
# Prepare a data diff
old_data = {}
new_data = {"my_data_key": "my_data_value"}
patch = jsonpatch.make_patch(old_data, new_data)
# https://github.com/ceramicnetwork/ceramic/blob/main/SPECIFICATION.md#signed-commits
commit_struct = {
"header": {
"family": "test",
"controllers": [did], # who is allowed to make updates
},
"data": patch.to_string(),
"prev": latest_cid,
"id": genesis_cid,
}
# Encode payload
encoded_bytes = dag_cbor.encode(commit_struct)
print("encoded", encoded_bytes)
hashed = hashlib.sha256(encoded_bytes).hexdigest()
print("hashed", hashed)
# Get commit_struct CID
cid = IPFSHashOnly.hash_bytes( # using IPFSHashOnly from open-aea
data=bytes(hashed,"utf-8"),
wrap = False,
cid_v1 = True
) # TODO: this could be what comes from Block.encode, but they also use the codec.code
# so our CID is probably wrong
print("cid", cid)
# Build the payload for signing
payload = {
"did": did,
"payload": cid,
"linkedBlock": encoded_bytes.hex(), # TODO: should we use hex here?
}
# Sign
signed = jws.sign(payload, did_seed, algorithm='HS256') # TODO: can we use here the seed from the DID generation like this?
print("signed", signed) # this looks like eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoiYiJ9.jiMyrsmD8AoHWeQgmxZ5yq8z0lXS67_QGs52AzC8Ru8
# It's similar to the "protected" field, but not to the "signature" one
# Build commit from the previous data # TODO
# For now, this is a copy of the working example from the docs
commit_data={
"jws": {
"payload": "AXESINm6lI30m3j5H2ausx-ulXj-L9CmFlOTZBZvJ2O734Zt", # is this the signed payload?
"signatures": [
{
"signature": "zsLJbBSU5xZTQkYlXwEH9xj_t_8frvSFCYs0SlVMPXOnw8zOJOsKnJDQlUOvPJxjt8Bdc_7xoBdmcRG1J1tpCw",
"protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2ZaNlM0TlZWVEV1dHM4bzV4RnpSTVI4ZUM2WTFibmdvQlFOblhpQ3ZoSDhII3o2TWtmWjZTNE5WVlRFdXRzOG81eEZ6Uk1SOGVDNlkxYm5nb0JRTm5YaUN2aEg4SCJ9"
}
],
"link": cid,
},
"linkedBlock": encoded_bytes.hex() # TODO: should we use hex here?
}
# Apply commit
code, data = cman.add_commit(streamid=streamid, commit=commit_data)
print(code)
print(data)
# I see here a 200 even for empty payloads. With correctly structured payloads, I get a 500: {'error': 'context deadline exceeded'}
# Get the latest commit and check it matches the changes
code, data = cman.get_commits(streamid)
print(code)
print(data["commits"][-1])
PS: is there any non-CLI JS example that I can follow step by step to be sure that every value matches?