Skip to content
This repository has been archived by the owner on Jul 13, 2023. It is now read-only.

Demo: download manager #84

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/boost-demo/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module test-server

go 1.20
38 changes: 38 additions & 0 deletions packages/boost-demo/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"bytes"
"io"
"log"
"net/http"
"time"
)

func main() {

resp, err := http.Get("http://magmo.com/nitro-protocol.pdf")
if err != nil {
panic(err)
}

file, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}

http.HandleFunc("/nitro-protocol.pdf", func(w http.ResponseWriter, r *http.Request) {

w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT")
w.Header().Set("Access-Control-Allow-Headers", "*")
if r.Method == "OPTIONS" {
_, _ = w.Write([]byte("OK"))
return
}

http.ServeContent(w, r, "nitro-protocol.pdf", time.Now(), io.NewSectionReader(bytes.NewReader(file), 0, int64(len(file))))
})

log.Fatal(http.ListenAndServe(":8081", nil))

}
249 changes: 85 additions & 164 deletions packages/boost-demo/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,193 +1,114 @@
import { ChangeEvent, useEffect, useState } from "react";
import { NitroRpcClient } from "@statechannels/nitro-rpc-client";
import { PaymentChannelInfo } from "@statechannels/nitro-rpc-client/src/types";
import {
Select,
MenuItem,
SelectChangeEvent,
Button,
TextField,
Box,
Table,
TableRow,
TableCell,
TableBody,
} from "@mui/material";
import { useState } from "react";
import { Box, Button } from "@mui/material";
import axios, { isAxiosError } from "axios";

const QUERY_KEY = "rpcUrl";

import "./App.css";

function App() {
const retrievalProvider = "0xbbb676f9cff8d242e9eac39d063848807d3d1d94";
const hub = "0x111a00868581f73ab42feef67d235ca09ca1e8db";
const defaultUrl = "localhost:4005";

const url =
new URLSearchParams(window.location.search).get(QUERY_KEY) ?? defaultUrl;
const numChunks = 3;
const length = 403507;

const [nitroClient, setNitroClient] = useState<NitroRpcClient | null>(null);
const [paymentChannels, setPaymentChannels] = useState<PaymentChannelInfo[]>(
[]
);
const [selectedChannel, setSelectedChannel] = useState<string>("");
const [selectedChannelInfo, setSelectedChannelInfo] = useState<
PaymentChannelInfo | undefined
>();
const [gotChunks, setGotChunks] = useState(new Array(numChunks).fill(false));

// TODO: For now the default is a hardcoded value based on a local file
// If you're running this locally you'll need to override this value
// Ideally we should just query boost/lotus for the list of available payloads>
const [payloadId, setPayloadId] = useState<string>(
"bafk2bzaceapnitekx4sp3mtitqatm5zpxn6nvjicwtltomttrlof65wlcfjpa"
);
function setChunkGot(index: number) {
const gotChunksCopy = [...gotChunks];
gotChunksCopy[index] = true;
setGotChunks(gotChunksCopy);
}

const [errorText, setErrorText] = useState<string>("");

useEffect(() => {
NitroRpcClient.CreateHttpNitroClient(url).then((c) => setNitroClient(c));
}, [url]);

// Fetch all the payment channels for the retrieval provider
useEffect(() => {
if (nitroClient) {
// TODO: We should consider adding a API function so this ins't as painful
nitroClient.GetAllLedgerChannels().then((ledgers) => {
for (const l of ledgers) {
if (l.Balance.Hub != hub) continue;

nitroClient.GetPaymentChannelsByLedger(l.ID).then((payChs) => {
const withProvider = payChs.filter(
(p) => p.Balance.Payee == retrievalProvider
);
setPaymentChannels(withProvider);
});
}
});
}
}, [nitroClient]);

const updateChannelInfo = async (channelId: string) => {
const paymentChannel = await nitroClient?.GetPaymentChannel(channelId);
setSelectedChannelInfo(paymentChannel);
};

const handleSelectedChannelChanged = async (event: SelectChangeEvent) => {
setSelectedChannel(event.target.value);
updateChannelInfo(event.target.value);
};

const updatePayloadId = (e: ChangeEvent<HTMLInputElement>) => {
setPayloadId(e.target.value);
};

const makePayment = () => {
setErrorText("");
if (nitroClient && selectedChannel) {
nitroClient.Pay(selectedChannel, 100);
// TODO: Slightly hacky but we wait a beat before querying so we see the updated balance
setTimeout(() => {
updateChannelInfo(selectedChannel);
}, 50);
async function getChunk(index: number, file: Int8Array[]) {
if (index >= numChunks) {
throw Error;
}
};

const fetchFile = () => {
setErrorText("");

axios
.get(
`http://localhost:7777/ipfs/${payloadId}?channelId=${selectedChannel}`,
const chunkSize = Math.floor(length / numChunks);
const startByte = index * chunkSize;
const endByte = Math.min(length, (index + 1) * chunkSize);
const range = "bytes=" + startByte + "-" + endByte;

try {
const result = await axios.get(
`http://localhost:8081/nitro-protocol.pdf`,
{
responseType: "blob", // This lets us download the file
headers: {
Accept: "*/*", // TODO: Do we need to specify this?
Range: range,
},
}
)

.then((result) => {
// This will prompt the browser to download the file
const blob = result.data;
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = blobUrl;
a.download = "fetched-file-from-ipfs";
a.click();
a.remove();
window.URL.revokeObjectURL(blobUrl);
})
.catch((e) => {
);
setChunkGot(index);
file[index] = result.data;
} catch {
// TODO check that the status code is 206 Partial Content
// It could be 200 OK -- but then we may have the entire file already!
(e: any) => {
if (isAxiosError(e)) {
setErrorText(`${e.message}: ${e.response?.statusText}`);
} else {
setErrorText(JSON.stringify(e));
}
});
};
}
}

function compileFile(file: Int8Array[]): File {
const f = new File(file, "nitro-protocol.pdf");
return f;
}

function triggerCompleteFileDownload(file: Int8Array[]) {
const fileUrl = URL.createObjectURL(compileFile(file));
const a = document.createElement("a");
a.href = fileUrl;
a.download = "nitro-protocol.pdf";
a.click();
a.remove();
window.URL.revokeObjectURL(fileUrl);
}

const fetchFileChunks = async () => {
setErrorText("");
const file = new Array<Int8Array>(numChunks);
for (let i = 0; i < numChunks; i++) {
await getChunk(i, file);
}

triggerCompleteFileDownload(file);
};

const fetchFileConcurrently = async () => {
setErrorText("");
const file = new Array<Int8Array>(numChunks);
const promises = [];
for (let i = 0; i < numChunks; i++) {
promises.push(getChunk(i, file));
}
await Promise.all(promises);
triggerCompleteFileDownload(file);
};

const fetchFileFull = async () => {
setErrorText("");
const file = new Array<Int8Array>(numChunks);
const result = await axios.get(`http://localhost:8081/nitro-protocol.pdf`, {
responseType: "blob", // This lets us download the file
headers: {
Accept: "*/*", // TODO: Do we need to specify this?
},
});
file[0] = result.data;

triggerCompleteFileDownload(file);
};
return (
<Box>
<Box p={10} minHeight={200}>
<Select
label="virtual channels"
onChange={handleSelectedChannelChanged}
value={selectedChannel}
>
{...paymentChannels.map((p) => (
<MenuItem value={p.ID}>{p.ID}</MenuItem>
))}
</Select>

<Table>
<TableBody>
<TableRow>
<TableCell>Paid so far</TableCell>
<TableCell>
{selectedChannelInfo &&
// TODO: We shouldn't have to cast to a BigInt here, the client should return a BigInt
BigInt(selectedChannelInfo?.Balance.PaidSoFar).toString(10)}
</TableCell>
</TableRow>
<TableRow>
<TableCell>Remaining funds</TableCell>
<TableCell>
{selectedChannelInfo &&
// TODO: We shouldn't have to cast to a BigInt here, the client should return a BigInt
BigInt(selectedChannelInfo?.Balance.RemainingFunds).toString(
10
)}
</TableCell>
</TableRow>
<TableRow>
<TableCell>Payee</TableCell>
<TableCell>
{selectedChannelInfo && selectedChannelInfo.Balance.Payee}
</TableCell>
</TableRow>
<TableRow>
<TableCell>Payer</TableCell>
<TableCell>
{selectedChannelInfo && selectedChannelInfo.Balance.Payer}
</TableCell>
</TableRow>
</TableBody>
</Table>
</Box>
<Box>
<TextField
fullWidth={true}
label="Payload Id"
onChange={updatePayloadId}
value={payloadId}
></TextField>
</Box>
<Box>
<Button onClick={makePayment}>Pay</Button>
<Button onClick={fetchFile}>Fetch</Button>
<Box>{errorText}</Box>
</Box>
{gotChunks.map((c) => (c ? "X" : "_"))}
<Button onClick={fetchFileFull}>Fetch</Button>
<Button onClick={fetchFileChunks}>FetchChunks</Button>
<Button onClick={fetchFileConcurrently}>FetchConcurrent</Button>
<Box>{errorText}</Box>
</Box>
);
}
Expand Down