Skip to main content

Requests and Operations

Up until now, all requests to Treasury returned more or less immediately. All these requests were actually for CRUD (Create, Read, Update, Delete) operations on resources of the treasury to occur.

In order to prevent individual users from being single points of compromise for security critical operations, in Treasury we distinguish:

  • requests (of an individual user, as identified by their signature with a valid credential, to perform a certain operation) from the
  • operations (that actually occur)

Via a configurable access policy we can require approvals of requests by users for operations by other users.

To have multiple users to work with, in addition to Alice let's create Bob:

set sign.with = root-key
create human user bob
bob-invite = create invite client key bob-invite { code = hex("bob-invite")}
create invite credential for bob { public_key = $bob-invite.public_key }

bob-key = create k256 client key bob-key
set sign.with = bob-invite
create k256 credential for bob { public_key = $bob-key.public_key }

Delegating to Alice

From the previous chapter, we still have two Solana addresses, one of which has a balance of around 4 SOL, the other with a balance of 1 SOL. We also have a transfer rule allowing movements from the first to the second -- but not in the other direction.

Let's assume Alice wishes to move that 1 SOL back to its original address.

set sign.with = alice-key
create transfer { from = $unfunded, to = $funded, asset = "SOL", amount ="1" }

will fail due to at least two reasons:

  • there is no access rule allowing Alice to request a "create Transfer" operation
  • there is no transfer rule allowing transfers on the given route

But also

set sign.with = alice-key
create allow transfer rule { from = $unfunded, to = $funded, asset = "SOL" }

will fail, because

  • there is no access rule allowing Alice to "create TransferRule".

Let's fix the first of these three issues:

set sign.with = root-key
alice-bob = create role alice-bob
update user alice { roles = [$alice-bob] }
update user bob { roles = [$alice-bob] }
create allow access rule { action = "create", resource = "Transfer", initiate = $alice-bob }

Notice that we snuck in the concept of a "role". For our purposes these are simply tags that can be attached to users (lines 3 + 4), and then later used to filter who may initiate operations, as in line 5.

Approvals

To now let Alice and Bob -- as a pair, but not individually -- extend the transfer policy, run:

create allow access rule { action = "create", resource = "TransferRule", initiate = "roles/alice-bob", approve = "roles/alice-bob", approvals = 1 }

In English, this reads as: allow users with the role "alice-bob" to create a transfer rule, if another user with the role "alice-bob" approves. Since currently only Alice and Bob have the alice-bob role, this is equivalent to either Alice requesting and Bob approving, or Bob requesting and Alice approving.

Let's try this out in practice immediately. In one terminal, run (warning: your interpreter will appear to hang, this is expected):

set sign.with = alice-key
create allow transfer rule { from = $unfunded, to = $funded, asset = "SOL" }

This will now not fail - but the interpreter will "hang". Why? The request is accepted (due to the access rule we created), but the corresponding operation does not immediately succeed (due to the approval requirement).

Behind the scenes, the interpreter is:

  • signing and sending the "create allow rule request"
  • receiving an operation ID from Treasury
  • polling with the operation ID until the operation either succeeded or failed

We can inspect this in another terminal. Run:

:: set sign.with = bob-key
:: operations | state = "authorizing" initiator = "users/alice"
authorizing operation 76DFFF46F2F1A2BA74BA750BC7557F537AB05FE5B67CEB2239F11AE97B568915 { version = 1, initiator = "users/alice", request = { treasury = "SstP567ZTAevDjWGHyKRph", api = 1, action = "create", resource = "TransferRule", body = "{\"variant\":\"allow\",\"asset\":\"chains/SOL/assets/SOL\",\"from\":\"chains/SOL/addresses/Vihj2YEBxvbAJM6iSxDAGC1ANESmGsiStdk8cJquJfW\",\"to\":\"chains/SOL/addresses/CirHy2hKuR3Yajy5K6qXo4xEkobftHCFNJKQswswdc1A\"}", user = "alice", create_time = 1715806929, nonce = 6162625399159, signature = "18f50a1fb7eec35c8a16091391a2794a6312f521f22c0e4f0bb13b7893c1cd1219f1b427d0eeb86054622c91964caed72c2658ca2340eae24042ee68aed2d9e5" } }

After inspecting this operation that is in authorizing state, we can decide to approve it:

approve 76DFFF46F2F1A2BA74BA750BC7557F537AB05FE5B67CEB2239F11AE97B568915

Two things will happen:

In Alice's terminal, the command will complete, and output the created transfer rule:

allow transfer-rule 28 { version = 1, asset = "chains/SOL/assets/SOL", from = "chains/SOL/addresses/Vihj2YEBxvbAJM6iSxDAGC1ANESmGsiStdk8cJquJfW", to = "chains/SOL/addresses/CirHy2hKuR3Yajy5K6qXo4xEkobftHCFNJKQswswdc1A" }

In Bob's terminal, the approve command will return without output. If Bob wishes to further track the operation, he can run

:: operation 76DFFF46F2F1A2BA74BA750BC7557F537AB05FE5B67CEB2239F11AE97B568915
succeeded operation 76DFFF46F2F1A2BA74BA750BC7557F537AB05FE5B67CEB2239F11AE97B568915 { version = 1, initiator = "users/alice", approve = ["users/bob"], request = { treasury = "SstP567ZTAevDjWGHyKRph", api = 1, action = "create", resource = "TransferRule", body = "{\"variant\":\"allow\",\"asset\":\"chains/SOL/assets/SOL\",\"from\":\"chains/SOL/addresses/Vihj2YEBxvbAJM6iSxDAGC1ANESmGsiStdk8cJquJfW\",\"to\":\"chains/SOL/addresses/CirHy2hKuR3Yajy5K6qXo4xEkobftHCFNJKQswswdc1A\"}", user = "alice", create_time = 1715806929, nonce = 6162625399159, signature = "18f50a1fb7eec35c8a16091391a2794a6312f521f22c0e4f0bb13b7893c1cd1219f1b427d0eeb86054622c91964caed72c2658ca2340eae24042ee68aed2d9e5" }, response = { name = "transfer-rules/28", version = 0 } }

Note that the operation is in "succeeded" state, and now has an additional field

response = { name = "transfer-rules/30", version = 0 }

Bob can further verify the transfer rule that was created by using this name:

::  transfer rule 28
allow transfer-rule 28 { version = 1, asset = "chains/SOL/assets/SOL", from = "chains/SOL/addresses/Vihj2YEBxvbAJM6iSxDAGC1ANESmGsiStdk8cJquJfW", to = "chains/SOL/addresses/CirHy2hKuR3Yajy5K6qXo4xEkobftHCFNJKQswswdc1A" }

Finally, the Transfer

Now either Alice and Bob can create the transfer we originally desired:

:: create transfer { from = $unfunded, to = $funded, asset = "SOL", amount ="1" }
preparing transfer 35 { version = 1, from = "chains/SOL/addresses/Vihj2YEBxvbAJM6iSxDAGC1ANESmGsiStdk8cJquJfW", to = "chains/SOL/addresses/CirHy2hKuR3Yajy5K6qXo4xEkobftHCFNJKQswswdc1A", asset = "chains/SOL/assets/SOL", amount = "1", last_transaction = { name = "transfers/35/transactions/36", state = "preparing", labels = { transaction = "" }, hash = "", input = "" } }