rocklabs-io / ic-py

Python Agent Library for the DFINITY Internet Computer
MIT License
128 stars 27 forks source link

Do you support delegations in signing certificates when making calls to canisters? #58

Open bodily11 opened 2 years ago

bodily11 commented 2 years ago

Would love to be able to pass in a delegation as a parameter to Identity or Canister or query_raw (or somewhere else) to be able to call on behalf of delegated principal. Is this possible right now?

bodily11 commented 2 years ago

I'm guessing no as when the signing occurs here:

https://github.com/rocklabs-io/ic-py/blob/7754db0ada112a409ec50221e85c6cbed0ab44c5/ic/agent.py#L15

In the envelope there is no option for sender_delegation, as defined here: https://smartcontracts.org/docs/current/references/ic-interface-spec/#authentication

Any plans to add sender_delegation?

Myse1f commented 2 years ago

Try to support delegation in #59. It is just a simple implementation.

You can switch to that branch and test. Using the ic_delegation and ic_identity in the local storage can construct an II delegation identity.

from ic.Identity import DelegationIdentity
from ic.Agent import Agent
from ic.Client import Client

# delegation and identity get from browser local storage
ic_delegation = """
{
    "delegations": [
        {
            "delegation": {
                "expiration": "xxx",
                "pubkey": "xxx"
            },
            "signature": "xxx"
        }
    ],
    "publicKey": "xxx"
}
"""
ic_identity = """
[
  "xxx",
  "xxx"
]
"""

client = Client()
iden = DelegateIdentity.from_json(ic_identity, ic_delegation)
print('principal:', Principal.self_authenticating(iden.der_pubkey))
ag = Agent(iden, client)
# now can use the agent to do query or update call

Remember that delegation identity from II has an expiry, thus it can't be used forever.

bodily11 commented 2 years ago

Oh nice! This is awesome! I'll test this out this week. And yeah, I know it has an expiry, but I'm going to use the II backend to actually add my script as a new device so then I have permanent authenticated access. I had to figure out authenticated/delegated II calls first though as you need those to register a new device.

bodily11 commented 2 years ago

All of my testing appears to be working on the delegation branch. Loading in a delegated identity using the information from II in the browser works great.

bodily11 commented 2 years ago

I just tried to pull some data from a different canister using the same feat_delegation branch I used for my other testing, and it gave me an error reading the data using the candid. When I switched back to main it worked again, so something is probably still off on the feat_delegation branch. Maybe you have tests you can run to spruce it up before merging those changes in to main. 😀 Just figured I would let you know.

Myse1f commented 2 years ago

Can you paste your error here? Some snippets that can reproduce it would be great!

bodily11 commented 2 years ago

Yeah, here's an error:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/var/folders/yg/mzpfyl291vx30knxqlx43d2r0000gn/T/ipykernel_39454/2771234657.py in <module>
     22 canister = Canister(agent=agent, canister_id=canister_id, candid=canister_did)
     23 
---> 24 result = canister.getCollectionDump()

/opt/anaconda3/lib/python3.9/site-packages/ic/canister.py in __call__(self, *args, **kwargs)
     55         effective_cansiter_id = args[0]['canister_id'] if self.canister_id == 'aaaaa-aa' and len(args) > 0 and type(args[0]) == dict and 'canister_id' in args[0] else self.canister_id
     56         if self.anno == 'query':
---> 57             res = self.agent.query_raw(
     58                 self.canister_id,
     59                 self.name,

/opt/anaconda3/lib/python3.9/site-packages/ic/agent.py in query_raw(self, canister_id, method_name, arg, return_type, effective_canister_id)
     77             raise ValueError("Malformed result: " + str(result))
     78         if result['status'] == 'replied':
---> 79             return decode(result['reply']['arg'], return_type)
     80         elif result['status'] == 'rejected':
     81             return result['reject_message']

/opt/anaconda3/lib/python3.9/site-packages/ic/candid.py in decode(data, retTypes)
   1293     for i, t in enumerate(types if retTypes == None else retTypes):
   1294         outputs.append({
-> 1295             'type': t.name,
   1296             'value': t.decodeValue(b, types[i])
   1297             })

/opt/anaconda3/lib/python3.9/site-packages/ic/candid.py in name(self)
    599     @property
    600     def name(self) -> str:
--> 601         return 'opt ({})'.format(str(self._type.name))
    602 
    603     @property

/opt/anaconda3/lib/python3.9/site-packages/ic/candid.py in name(self)
    555     @property
    556     def name(self) -> str:
--> 557         return 'vec ({})'.format(str(self._type.name))
    558 
    559     @property

/opt/anaconda3/lib/python3.9/site-packages/ic/candid.py in name(self)
    677     @property
    678     def name(self) -> str:
--> 679         fields = ";".join(map(lambda kv: kv[0] + ":" + kv[1].name, self._fields.items()))
    680         return "record {{{}}}".format(fields)
    681 

/opt/anaconda3/lib/python3.9/site-packages/ic/candid.py in <lambda>(kv)
    677     @property
    678     def name(self) -> str:
--> 679         fields = ";".join(map(lambda kv: kv[0] + ":" + kv[1].name, self._fields.items()))
    680         return "record {{{}}}".format(fields)
    681 

/opt/anaconda3/lib/python3.9/site-packages/ic/candid.py in name(self)
    555     @property
    556     def name(self) -> str:
--> 557         return 'vec ({})'.format(str(self._type.name))
    558 
    559     @property

/opt/anaconda3/lib/python3.9/site-packages/ic/candid.py in name(self)
    677     @property
    678     def name(self) -> str:
--> 679         fields = ";".join(map(lambda kv: kv[0] + ":" + kv[1].name, self._fields.items()))
    680         return "record {{{}}}".format(fields)
    681 

/opt/anaconda3/lib/python3.9/site-packages/ic/candid.py in <lambda>(kv)
    677     @property
    678     def name(self) -> str:
--> 679         fields = ";".join(map(lambda kv: kv[0] + ":" + kv[1].name, self._fields.items()))
    680         return "record {{{}}}".format(fields)
    681 

TypeError: can only concatenate str (not "int") to str

And this happens when I call a few methods. Let me find you a public method to call to reproduce the error.

bodily11 commented 2 years ago

Here's a public reproducible error for you:

# governance_did can be downloaded from canlista here: 
# https://k7gat-daaaa-aaaae-qaahq-cai.ic0.app/listing/nns-governance-10222/qoctq-giaaa-aaaaa-aaaea-cai
governance_canister_id = 'rrkah-fqaaa-aaaaa-aaaaq-cai'
i1 = Identity()
client = Client(url = "https://ic0.app")
agent = Agent(i1, client)
testCanister = Canister(agent=agent, canister_id=governance_canister_id, candid=governance_did)
testCanister.get_monthly_node_provider_rewards()
bodily11 commented 2 years ago

Just as a follow-up, this code works fine on the main branch, but breaks with the error above on the feat-delegation branch. Maybe due to the type table parsing fix you pull into it to fix the other bug.

Myse1f commented 2 years ago

Should be fixed now. Thanks!

bodily11 commented 2 years ago

Yeah perfect. That solved the issue. Running into one more issue.

This is the governance canister (same .did as above)

governanceCanister.manage_neuron(
    {
    'id':[{
        'id':all_icp_neurons[0]
    }],

    'command':[{'MergeMaturity':{
        'percentage_to_merge':1
        }
    }],

    'neuron_id_or_subaccount':[]
    })

In this case, I get the following error:

TypeError                                 Traceback (most recent call last)
/var/folders/yg/mzpfyl291vx30knxqlx43d2r0000gn/T/ipykernel_41783/2057413411.py in <module>
----> 1 governanceCanister.manage_neuron(
      2     {
      3     'id':[{
      4         'id':all_icp_neurons[0]
      5     }],

/opt/anaconda3/lib/python3.9/site-packages/ic/canister.py in __call__(self, *args, **kwargs)
     66                 self.canister_id,
     67                 self.name,
---> 68                 encode(arguments),
     69                 self.rets,
     70                 effective_cansiter_id

/opt/anaconda3/lib/python3.9/site-packages/ic/candid.py in encode(params)
   1258     for i in range(len(args)):
   1259         t = argTypes[i]
-> 1260         if not t.covariant(args[i]):
   1261             raise TypeError("Invalid {} argument: {}".format(t.display(), str(args[i])))
   1262         vals += t.encodeValue(args[i])

/opt/anaconda3/lib/python3.9/site-packages/ic/candid.py in covariant(self, x)
    835 
    836     def covariant(self, x):
--> 837         return self._type if self._type.covariant(x) else False
    838 
    839     def encodeValue(self, val):

/opt/anaconda3/lib/python3.9/site-packages/ic/candid.py in covariant(self, x)
    630             if not k in x:
    631                 raise ValueError("Record is missing key {}".format(k))
--> 632             if v.covariant(x[k]):
    633                 continue
    634             else:

/opt/anaconda3/lib/python3.9/site-packages/ic/candid.py in covariant(self, x)
    571 
    572     def covariant(self, x):
--> 573         return type(x) == list and (len(x) == 0 | (len(x) == 1 and self._type.covariant(x[0])))
    574 
    575     def encodeValue(self, val):

TypeError: unsupported operand type(s) for |: 'int' and 'RecordClass'

Usually if I have a formatting error between types/values in candid I get a record formatting error, so it seems my values match the candid. But for some reason it is failing to correctly encode the data using the candid. So I thought maybe an ic-py issue?

Myse1f commented 2 years ago

Fixed! Thank you very much for the detail.

I think we should test it more carefully and thoroughly.

bodily11 commented 2 years ago

Yeah, that fixes it. Thanks! Everything seems to be working now on feat-delegation branch. I'm able to authenticate with II across apps and automate process. So I'm in really good shape. Thanks for all the help.

When are you thinking about merging this branch into production? Would love to get it in sooner rather than later (assuming all bugs have been fixed) to prevent too many conflicts from the main repo.