Skip to content
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

Jake inv4 git comments #23

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
37 changes: 31 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,41 +27,51 @@ mod util;
#[subxt(runtime_metadata_path = "invarch_metadata.scale")]
pub mod invarch {}

pub async fn set_repo(
/// Gets Repo Data file from IP Set and returns a `RepoData` struct created from the file.
/// If no file, return `RepoData` struct with defaults
pub async fn get_repo(
ips_id: u32,
api: invarch::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>,
) -> BoxResult<RepoData> {
// Setup IPFS client
let mut ipfs_client = IpfsClient::default();

// Get assets (IPFs, NFTs, nested IPS, NFT collections) under `ips_id` IP Set
let data = api
.storage()
.inv4()
.ip_storage(&ips_id, None)
.inv4() // pallet
.ip_storage(&ips_id, None) // storage entity
.await?
.ok_or(format!("Ips {ips_id} does not exist"))?
.data
.0;

// Find "RepoData" IPF and return `RepoData` struct if it exists
for file in data {
if let AnyId::IpfId(id) = file {
let ipf_info = api
.storage()
.ipf()
.ipf_storage(&id, None)
.ipf() // pallet
.ipf_storage(&id, None) // storage entity
.await?
.ok_or("Internal error: IPF listed from IPS does not exist")?;
if String::from_utf8(ipf_info.metadata.0.clone())? == *"RepoData" {
return RepoData::from_ipfs(ipf_info.data, &mut ipfs_client).await;
}
}
}

// Return default `RepoData` if file doesn't exist
Ok(RepoData {
refs: Default::default(),
objects: Default::default(),
})
}

/// Git will call this helper program because it does not natively support git-remote-inv4
#[tokio::main]
async fn main() -> BoxResult<()> {
// Get URL passed from Git.
let (_, raw_url) = {
let mut args = args();
args.next();
Expand All @@ -71,16 +81,19 @@ async fn main() -> BoxResult<()> {
)
};

// Parse `ips_id` and sub token ID (optional) from URL
let (ips_id, subasset_id) = {
let mut url = Path::new(&raw_url).components();
url.next();
(
// Get IPS ID
url.next()
.ok_or("Missing IPS id. Expected: 'inv4://>ips_id<'")?
.as_os_str()
.to_str()
.ok_or("Input was not UTF-8")?
.parse::<u32>()?,
// Get optional sub token ID
if let Some(component) = url.next() {
Some(
component
Expand All @@ -101,7 +114,9 @@ async fn main() -> BoxResult<()> {

std::fs::create_dir_all(config_file_path.parent().unwrap()).unwrap();

// Deserialize `config.toml` into `Config` struct
let config: Config = if config_file_path.exists() {
// Read contents of config.toml file into `contents` buffer
let mut contents = String::new();
std::fs::File::options()
.write(true)
Expand All @@ -112,6 +127,7 @@ async fn main() -> BoxResult<()> {

toml::from_str(&contents)?
} else {
// Default Substrate RPC server address
let c = Config {
chain_endpoint: String::from("ws://127.0.0.1:9944"),
};
Expand All @@ -123,19 +139,22 @@ async fn main() -> BoxResult<()> {
c
};

// Create subxt client connected to specified chain
let api: invarch::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>> =
ClientBuilder::new()
.set_url(config.chain_endpoint)
.build()
.await?
.to_runtime_api();

let mut remote_repo = set_repo(ips_id, api.clone()).await?;
// Get IPS RepoData
let mut remote_repo = get_repo(ips_id, api.clone()).await?;
debug!("RepoData: {:#?}", remote_repo);

loop {
let repo = Repository::open_from_env().unwrap();

// Read next input command
let mut input = String::new();
io::stdin().read_line(&mut input)?;

Expand Down Expand Up @@ -192,6 +211,7 @@ async fn push(
mut ipfs: IpfsClient,
ref_arg: &str,
) -> BoxResult<()> {
// Requesting credentials from the user in the terminal
let mut cmd = Command::new("git");
cmd.arg("credential");
cmd.arg("fill");
Expand Down Expand Up @@ -241,6 +261,7 @@ async fn push(
error!("No credential")
}

// Generate key pair from command line entered seed phrase
let signer = &PairSigner::<DefaultConfig, sp_keyring::sr25519::sr25519::Pair>::new(
sp_keyring::sr25519::sr25519::Pair::from_string(&credential, None).unwrap(),
);
Expand Down Expand Up @@ -273,10 +294,12 @@ async fn push(
.await
{
Ok(pack_ipf_id) => {
// Get IPF ID's for new and potential old RepoData files
let (new_repo_data, old_repo_data) = remote_repo
.mint_return_new_old_id(&mut ipfs, api, signer, ips_id)
.await?;

// If IP Set has a pre-existing RepoData file, remove it from the IP Set
if let Some(old_id) = old_repo_data {
eprintln!("Removing old Repo Data with IPF ID: {}", old_id);

Expand All @@ -286,6 +309,7 @@ async fn push(
new_metadata: None,
});

// Sign and submit the `remove_call` extrinsic to remove the old RepoData IPF from the IPS
api.tx()
.inv4()
.operate_multisig(false, (ips_id, subasset_id), remove_call)?
Expand All @@ -304,6 +328,7 @@ async fn push(
new_metadata: None,
});

// Sign and submit the `append_call` extrinsic to add the new RepoData IPF and the new MultiObject to the IPS
api.tx()
.inv4()
.operate_multisig(true, (ips_id, subasset_id), append_call)?
Expand Down
28 changes: 22 additions & 6 deletions src/primitives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub static SUBMODULE_TIP_MARKER: &str = "submodule-tip";

pub type BoxResult<T> = Result<T, Box<dyn Error>>;

/// Holds multiple git objects in a single struct
#[derive(Clone, Debug, Encode, Decode)]
pub struct MultiObject {
pub hash: String,
Expand Down Expand Up @@ -79,6 +80,8 @@ impl MultiObject {
}
}

/// Represents a git object. Types are Commit, Tag, Tree, & Blob
/// Ex in filesystem: .git/objects/4b/62c9e0f3c6550c17af27daa0b24a194e113374
#[derive(Clone, Debug, Encode, Decode)]
pub struct GitObject {
/// The git hash of the underlying git object
Expand All @@ -89,18 +92,19 @@ pub struct GitObject {
pub metadata: GitObjectMetadata,
}

// Object metadata, necessary for finding other objects they reference
#[derive(Clone, Debug, Encode, Decode)]
pub enum GitObjectMetadata {
#[allow(missing_docs)]
/// References tree and its parent commit
Commit {
parent_git_hashes: BTreeSet<String>,
tree_git_hash: String,
},
#[allow(missing_docs)]
/// References a specific commit
Tag { target_git_hash: String },
#[allow(missing_docs)]
/// References blobs and/or other trees
Tree { entry_git_hashes: BTreeSet<String> },
#[allow(missing_docs)]
/// The actual files of the repo i.e. .html, .js, .pdf, etc.
Blob,
}

Expand Down Expand Up @@ -161,15 +165,18 @@ impl GitObject {
}
}

/// Top level repository data
#[derive(Encode, Decode, Debug, Clone)]
pub struct RepoData {
/// All refs this repository knows; a {name -> sha1} map
/// All refs this repository knows; a {branch name -> sha1 (git hash)} map
/// i.e. branches
pub refs: BTreeMap<String, String>,
/// All objects this repository contains; a {sha1 -> MultiObject hash} map
/// All objects this repository contains; a {sha1 (git hash) -> MultiObject hash} map
pub objects: BTreeMap<String, String>,
}

impl RepoData {
/// Get the serialized RepoData file from IPFS and deserialize it, returning a `RepoData` struct
pub async fn from_ipfs(ipfs_hash: H256, ipfs: &mut IpfsClient) -> Result<Self, Box<dyn Error>> {
let refs_cid = generate_cid(ipfs_hash)?.to_string();
let refs_content = ipfs
Expand Down Expand Up @@ -581,6 +588,8 @@ impl RepoData {
}

debug!("Pushing MultiObject to IPFS");
// Actually push data to IPFS and get the unique hash back
// First 2 bytes are multihash metadata and are excluded b/c not part of the actual hash (digest)
let ipfs_hash = &Cid::try_from(ipfs.add(Cursor::new(multi_object.encode())).await?.hash)?
.to_bytes()[2..];

Expand Down Expand Up @@ -675,13 +684,16 @@ impl RepoData {
Ok(())
}

/// Mint new/updated RepoData file.
/// Returns IPF ID of new file and Option holding ID of potential pre-existing file
pub async fn mint_return_new_old_id(
&self,
ipfs: &mut IpfsClient,
chain_api: &invarch::RuntimeApi<DefaultConfig, PolkadotExtrinsicParams<DefaultConfig>>,
signer: &PairSigner<DefaultConfig, sp_keyring::sr25519::sr25519::Pair>,
ips_id: u32,
) -> Result<(u64, Option<u64>), Box<dyn Error>> {
// Mint `RepoData` instance as a new IPF
let events = chain_api
.tx()
.ipf()
Expand All @@ -697,6 +709,7 @@ impl RepoData {
.wait_for_in_block()
.await?;

// Get ID of new IPF just minted
let new_ipf_id = events
.fetch_events()
.await?
Expand All @@ -708,13 +721,15 @@ impl RepoData {

eprintln!("Minted Repo Data on-chain with IPF ID: {}", new_ipf_id);

// Get IPS info
let ips_info = chain_api
.storage()
.inv4()
.ip_storage(&ips_id, None)
.await?
.ok_or(format!("IPS {ips_id} does not exist"))?;

// Check if IPS has a pre-existing RepoData file
for file in ips_info.data.0 {
if let AnyId::IpfId(id) = file {
let ipf_info = chain_api
Expand All @@ -729,6 +744,7 @@ impl RepoData {
}
}

// IPS doesn't have a pre-existing RepoData file
Ok((new_ipf_id, None))
}
}