-
Notifications
You must be signed in to change notification settings - Fork 90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Upload New AppData to IPFS #2583
Conversation
CLA Assistant Lite bot All contributors have signed the CLA ✍️ ✅ |
I have read the CLA Document and I hereby sign the CLA |
@@ -18,7 +18,7 @@ pub struct ValidatedAppData { | |||
pub protocol: ProtocolAppData, | |||
} | |||
|
|||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] | |||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These were added across a bunch of structures in order to "Serialize" the AppData structure as JSON for post.
@@ -60,7 +60,13 @@ impl Registry { | |||
.insert_full_app_data(&validated.hash, &validated.document) | |||
.await | |||
{ | |||
Ok(()) => Ok((Registered::New, validated.hash)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We intercept the successful DB insertion to post to IPFS.
Ok(()) => { | ||
// Post New AppData to IPFS after successful insert. | ||
if let Some(ipfs) = &self.ipfs { | ||
let _ = ipfs.post_app_data(validated.clone()).await; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could probably make use of the result here, but didn't have any thoughts on how you would like to do it. Maybe we can move the logging out of the inner methods to here...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case this operation is not going to be executed atomically with the DB insert, how about running the IPFS request asynchronously(probably, with a retry ability)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This suggestion sounds a bit like making use of this existing project https://github.com/cowprotocol/ipfs-block-put which can be running as a separate service whose only responsibility receive upload requests and handle them. Then here, we would just send out our requests to upload and forget about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree that using the block put service optimistically (ie in a fire/forget manner) make sense for implementing this.
It looks like ipfs-block-put
doesn't currently support retry nor prometheus metrics, which would be needed though to ensure we can have proper alerting in case it breaks.
pub id: String, | ||
pub ipfs_hash: String, | ||
pub status: String, | ||
pub name: Option<String>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name is entirely unused by this code change and could be left out. Could use the hex appdata hash as the name.
if let Some(query) = &self.query { | ||
url.set_query(Some(query.as_str())); | ||
if let Some(jwt) = &self.auth_token { | ||
url.set_query(Some(&format!("pinataGatewayToken={}", jwt))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This subtle change allows the auth token to be used for the private gateway both for fetching and for uploading. The way this was formatted before was bound to the "fetch" endpoint.
auth_token: Option<String>, | ||
} | ||
|
||
#[derive(Serialize, Deserialize, Debug, PartialEq)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the request need to be deserialized on our end?
|
||
#[derive(Serialize, Deserialize, Debug, PartialEq)] | ||
#[serde(rename_all = "camelCase")] | ||
pub struct PinByHashRequest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Public visibility not need I believe
pub pinata_metadata: String, | ||
} | ||
|
||
#[derive(Debug, Serialize, Deserialize, PartialEq)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the response need to be serializable?
} | ||
|
||
impl Ipfs { | ||
pub fn new(client: ClientBuilder, base: Url, query: Option<String>) -> Self { | ||
pub fn new(client: ClientBuilder, base: Url, auth_token: Option<String>) -> Self { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is coupling the IPFS component too tightly to Pinata imo. Before it would work with any gateway, now it only works with Pinata.
Ok(()) => { | ||
// Post New AppData to IPFS after successful insert. | ||
if let Some(ipfs) = &self.ipfs { | ||
let _ = ipfs.post_app_data(validated.clone()).await; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree that using the block put service optimistically (ie in a fire/forget manner) make sense for implementing this.
It looks like ipfs-block-put
doesn't currently support retry nor prometheus metrics, which would be needed though to ensure we can have proper alerting in case it breaks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly concerned with ensuring that what we pin on IPFS actually ends up with the same hash we have in our DB.
pub async fn add(&self, data: PinByHashRequest) -> Result<PinResponse> { | ||
let url = format!("{}pinning/pinByHash", self.base); | ||
// Prevent upload attempt when `auth_token is None`. | ||
let jwt = if let Some(jwt) = &self.auth_token { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Should be equivalent to sth like:
let jwt = self.auth_token.context("Can't post without valid JSON web token")?;
Ok(response) | ||
} else { | ||
Err(anyhow!( | ||
"Error posting AppData to IPFS: {:?}", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would also be nice to get the status code in the error.
if resp.status().is_success() { | ||
let data = resp.text().await?; | ||
let response: PinResponse = serde_json::from_str(&data)?; | ||
tracing::debug!("AppData pinned to IPFS with CID: {}", response.ipfs_hash); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: We usually try to avoid dynamic log strings to make searching more consistent.
tracing::debug!("AppData pinned to IPFS with CID: {}", response.ipfs_hash); | |
tracing::debug!(cid = response.ipfs_hash, "AppData pinned to IPFS"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! I had been wondering about what that was all about!
// Format of pin_metadata this MUST BE as follows: | ||
// {"keyvalues":{"appData":"{\"appCode\":\"CoW Swap\"}"}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think what actually needs to be stored is just the value of appData
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried... they expect a key. Then we would have to make every inner json document an escaped string.
@@ -118,6 +119,26 @@ impl IpfsAppData { | |||
metric.with_label_values(&[outcome(&result), "node"]).inc(); | |||
Ok(result) | |||
} | |||
|
|||
pub async fn post_app_data(&self, data: ValidatedAppData) -> Result<PinResponse> { | |||
tracing::debug!("posting appData to IPFS {:?}", data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tracing::debug!("posting appData to IPFS {:?}", data); | |
tracing::debug!(?data, "posting to IPFS"); |
let parsed_json: Value = serde_json::from_str(&value.document).unwrap(); | ||
// build escaped json string. | ||
let escaped_json_string = serde_json::to_string(&parsed_json.to_string()).unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since already have a ValidatedAppData
we should no longer have to parse the content to verify it's valid JSON.
Also we should use the value.document
as is instead of deserializing and re-serializing since this might change its representation which can result in a different CID.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Problem with document is that it's not escaped properly for ipfs api.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, strange that they require a specific format for the content. Btw when looking into pinByHash they say it only pins document that have already been uploaded to IPFS. It looks like what we'd actually want to use is pinFileToIpfs to actually make pinata upload the file.
Or alternatively as discussed in some other comment using https://github.com/cowprotocol/ipfs-block-put should also work with the raw strings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ya maybe I misunderstood what uploading a file to ipfs is. I guess pinning is not the same thing. Or needs to be done after. My thought was that pin by hash would upload the document and link it to our specific cid.
Anyway, I can't touch this again till the weekend and it seems to be a whole lot more involved than I was expecting.
This pull request has been marked as stale because it has been inactive a while. Please update this pull request or it will be automatically closed. |
This pull request has been marked as stale because it has been inactive a while. Please update this pull request or it will be automatically closed. |
This PR closes #2302 by implementing a
post_app_data
method on theIpfsAppData
structure (which internally calls a newly implementedIpfs.add
. This is just a raw http-requestPOST
request to this Pinata Endpoint.This is done by introducing a structure with correctly formatted payload that is accepted by the API endpoint (see unit tests for the specific format). Constructor method are in place to manufacture the specific format and all possible routes are tested (however only the expected payload format is included in a test that isn't ignored).
Notes on Decisions Made
appDataHash
.cc @fleupold (as the creator or the related issue).