Some Circles tricks

If you (like me!) have skipped the Circles web application and prefer interacting directly with smart contracts on the xDAI blockchain, you’ll have to do some work to understand your trust graph in order to reciprocate connections, etc.

As a reminder, my handmade Circles identity is:

>>>>>>> 0x647E68F4BCBC843F39c80bb02da96dD13308f657

Pathfinder

A first resource to take a look at is the Pathfinder web app by @ethchris (see also chriseth/pathfinder on Github).

With this, you can explore your own (or anyone else’s) trustgraph:

You can also compute paths through which you can “hub transfer” tokens, which would be accepted on a one-to-one basis by the recipient.

Circles API

Circles offers a traditional REST-ish JSON API.

The API base URL is https://api.circles.garden/api/

The API is well documented here. For example, an easy trick is to look up a usernames. Hitting the URL https://api.circles.garden/api/users?query=poop yields…

{"status":"ok","data":[{"id":180315,"username":"elpoopacabrah","safeAddress":"0x359b1De9bda75A79292f9871B085eDdAB71Bc2B4","avatarUrl":"https://circles-ubi.s3.amazonaws.com/uploads/avatars/8dac2b5b38141a110b457b375fd9fc7595509fbb7ebe2336f95d425c1987463a.jpg"},{"id":131656,"username":"poope38","safeAddress":"0xE7D998548dff5a7a9186Abb96579708E32BCA85C","avatarUrl":null},{"id":15919,"username":"Poopie","safeAddress":"0x9268c0D32547287dF8d792FEc060bb1D6ccb2B8D","avatarUrl":null},{"id":4272,"username":"Poopman12536","safeAddress":"0x4076EF4CeAE0C0D4F2a77A478d6e0846aF8aE89C","avatarUrl":null},{"id":89656,"username":"poopsuth727","safeAddress":"0x9b628f2A15937940191B516d116F85dF621e68Ea","avatarUrl":null},{"id":90450,"username":"spoop","safeAddress":"0xd75B5EB59255D000a3F21a23dbdDC0d2C397de80","avatarUrl":"https://circles-ubi.s3.amazonaws.com/uploads/avatars/b06e5795281609b4d2360a1e05e80468acd3c68aa60e18b6f54f0f9f3183f2a8.jpg"}]}

(There’s a lot here; scroll right!)

The address that represents a user’s Circles identity is shown as safeAddress.

This is helpful, because the Pathfinder app shows usernames only, but if you want to reciprocate trust relationships you’ll have to map that back to an Ethereum address identity.

You can also look up by address, but if you circumvented the web-app and created your identity on the blockchain, it won’t show up. So…

https://api.circles.garden/api/users?address[]=0x9b628f2A15937940191B516d116F85dF621e68Ea

{"status":"ok","data":[{"id":89656,"username":"poopsuth727","safeAddress":"0x9b628f2A15937940191B516d116F85dF621e68Ea","avatarUrl":null}]}

But with my Circles identity…

https://api.circles.garden/api/users?address[]=0x647E68F4BCBC843F39c80bb02da96dD13308f657

{"status":"ok","data":[]}

There is a PUT /api/users endpoint that hopefully will remedy this, but I haven’t been able to make it work. (See the appendix.)

Circles subgraph on TheGraph

TheGraph is a large project by which a large array of Ethereum / Web3 projects publish information about themselves using a standard-ish GraphQL interface.

I haven’t really played with it, but there’s an “explorer” for Circles at https://thegraph.com/explorer/subgraph/circlesubi/circles. Pathfinder downloads basically complete information about the Circles user graph (denoted “safes”, since the web app represents users as Gnosis Safe contracts).

The GraphQL API is accessed at https://graph.circles.garden/subgraphs/name/CirclesUBI/circles

Appendix: Unsuccessful attempt to PUT /api/users

Normally _Circles- identities are addresses of Gnosis Safe contracts created by the web application. But looking at the docs for PUT /api/users, it looks possible to create a web-app identity and avatar even after a Circles identity has been established. So I tried it. First, from within sbt-ethereum, I (dangerously) extracted my plaintext private key using the ethKeystorePrivateKeyReveal task.

> ethKeystorePrivateKeyReveal 0x647E68F4BCBC843F39c80bb02da96dD13308f657
[info] V3 wallet(s) found for '0x647E68F4BCBC843F39c80bb02da96dD13308f657' (aliases ['circles-identity'])
Enter passphrase or hex private key for address '0x647E68F4BCBC843F39c80bb02da96dD13308f657': ************************
Are you sure you want to reveal the unencrypted private key on this very insecure console? [Type YES exactly to continue, anything else aborts]: YES
0xNOFUCKINGWAYIMACTUALLYSHOWINGTHISTOYOU
[success] Total time: 33 s, completed Apr 19, 2021, 8:40:37 PM

Then I’m supposed to PUT to /api/users a JSON object that contains a signed concatenation of address + nonce + safeAddress + username. I’ve presumed all of these should be treated as strings (with no 0x prefix for hex values), although I expected I might have to try different conventions maybe. Nonce is to be omitted from the request and replaced with 0 in the signature if you don’t intend for the API to build a safe for you, so that’s what I did. Anyway, to get the signature, I had to drop into a scala console from my sbt-ethereum project. consoleProject in sbt drops you into a console with all the libraries used by the build tool (rather than the dependencies of the thing being built) available. Always type <return> a few times after starting consoleProject. Otherwise whatever you type gets lost in a lot of imports.

> consoleProject
[info] Starting scala interpreter...
Welcome to Scala 2.12.12 (Java HotSpot(TM) 64-Bit Server VM, Java 11.0.3).
Type in expressions for evaluation. Or try :help.

scala> import _root_.scala.xml.{TopScope=>$scope}
import _root_.sbt._
import _root_.sbt.Keys._
import _root_.sbt.nio.Keys._
import _root_.sbt.ScriptedPlugin.autoImport._
import _root_.sbt.plugins.MiniDependencyTreePlugin.autoImport._
import _root_.com.mchange.sc.v1.sbtethereum.SbtEthereumPlugin.autoImport._
import _root_.net.virtualvoid.sbt.graph.DependencyGraphPlugin.autoImport._
import _root_.com.typesafe.sbt.SbtPgp.autoImport._
import _root_.sbt.plugins.IvyPlugin
import _root_.sbt.plugins.JvmPlugin
import _root_.sbt.plugins.CorePlugin
import _root_.sbt.ScriptedPlugin
import _root_.sbt.plugins.SbtPlugin
import _root_.sbt.plugins.SemanticdbPlugin
import _root_.sbt.plugins.JUnitXmlReportPlugin
import _root_.sbt.plugins.Giter8TemplatePlugin
import _root_.sbt.plugins.MiniDependencyTreePlugin
imp...


scala> 

scala>

scala> import com.mchange.sc.v1.consuela._
import com.mchange.sc.v1.consuela._

scala> import com.mchange.sc.v1.consuela.ethereum._
import com.mchange.sc.v1.consuela.ethereum._

scala> val signer = EthPrivateKey("0xNOFUCKINGWAYIMACTUALLYSHOWINGTHISTOYOU")
signer: com.mchange.sc.v1.consuela.ethereum.EthPrivateKey = EthPrivateKey(ByteSeqExact32(<masked>))

scala> val signMe = ("647e68f4bcbc843f39c80bb02da96dd13308f657" + "0" + "647e68f4bcbc843f39c80bb02da96dd13308f657" + "interfluidity").getBytes("UTF8")
signMe: Array[Byte] = Array(54, 52, 55, 101, 54, 56, 102, 52, 98, 99, 98, 99, 56, 52, 51, 102, 51, 57, 99, 56, 48, 98, 98, 48, 50, 100, 97, 57, 54, 100, 100, 49, 51, 51, 48, 56, 102, 54, 53, 55, 48, 54, 52, 55, 101, 54, 56, 102, 52, 98, 99, 98, 99, 56, 52, 51, 102, 51, 57, 99, 56, 48, 98, 98, 48, 50, 100, 97, 57, 54, 100, 100, 49, 51, 51, 48, 56, 102, 54, 53, 55, 105, 110, 116, 101, 114, 102, 108, 117, 105, 100, 105, 116, 121)

scala> signer.sign( signMe )
res0: com.mchange.sc.v1.consuela.ethereum.EthSignature.Basic = Basic(SignatureV(27),SignatureR(30823757904115583883589356676902125373287900743828297884858325527266347865052),SignatureS(5273297983938010099410364373834334448821941639324227679042884161620989105997))

scala> val signature = signer.sign( signMe )
signature: com.mchange.sc.v1.consuela.ethereum.EthSignature.Basic = Basic(SignatureV(27),SignatureR(40577138781298238809142157285870788246321445791081483798092175929564989797163),SignatureS(15708058885740351184013345937938646231244742541343868474640074509830709578783))

scala> signature.exportBytesVRS
res1: com.mchange.sc.v1.consuela.ethereum.specification.Types.ByteSeqExact65 = ByteSeqExact65(0x1b59b5d8bf72ea24db3aa2eaf1ad969cd1646c1f6213910c91b1201737e464b32b22ba723e57ee82c07793be28c356b0ab632af69c568501092e59c6e281f80c1f)

scala> signature.exportBytesRSV
res2: com.mchange.sc.v1.consuela.ethereum.specification.Types.ByteSeqExact65 = ByteSeqExact65(0x59b5d8bf72ea24db3aa2eaf1ad969cd1646c1f6213910c91b1201737e464b32b22ba723e57ee82c07793be28c356b0ab632af69c568501092e59c6e281f80c1f1b)

Great. Then I dropped into a shell, and tried curl, trying both VRS and RSV signature formats:

$ curl --header 'Content-Type: application/json' --request PUT --data '{ "address":"647e68f4bcbc843f39c80bb02da96dd13308f657","signature":"1b59b5d8bf72ea24db3aa2eaf1ad969cd1646c1f6213910c91b1201737e464b32b22ba723e57ee82c07793be28c356b0ab632af69c568501092e59c6e281f80c1f","data":{"safeAddress":"647e68f4bcbc843f39c80bb02da96dd13308f657","email":"swaldman@mchange.com","username":"interfluidity","avatarURL":"https://avatars.githubusercontent.com/u/1733981?s=400&u=95ab8f59d53b00c98d77f1206be1be086ffa6aef&v=4"} }' https://api.circles.garden/api/users
{"status":"error","code":400,"message":"Bad Request"}

$ curl --header 'Content-Type: application/json' --request PUT --data '{ "address":"647e68f4bcbc843f39c80bb02da96dd13308f657","signature":"59b5d8bf72ea24db3aa2eaf1ad969cd1646c1f6213910c91b1201737e464b32b22ba723e57ee82c07793be28c356b0ab632af69c568501092e59c6e281f80c1f1b","data":{"safeAddress":"647e68f4bcbc843f39c80bb02da96dd13308f657","email":"swaldman@mchange.com","username":"interfluidity","avatarURL":"https://avatars.githubusercontent.com/u/1733981?s=400&u=95ab8f59d53b00c98d77f1206be1be086ffa6aef&v=4"} }' https://api.circles.garden/api/users
{"status":"error","code":400,"message":"Bad Request"}

Lots of things could be wrong with my signature. Maybe the addresses were not supposed to be concatenated as hex strings (without 0x), but were supposed to be converted to bytes before signing. Maybe the nonce was supposed to be formatted as a 32 bytes 0 rather than the character 0. Maybe I was supposed to sign the raw bytes of the string, rather than its Keccak256 hash as is conventional in Ethereum.

But according to the docs, “Verification failed” was supposed to give a 403 response code. That doesn’t seem to be what’s happening. 400 is for “Parameters missing or malformed”. I can’t find any parameters missing or malformed (except for nonce, which is defined as optional). So, I’m flummoxed for the moment.