diff --git a/README.md b/README.md index b31ce37..f673037 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,7 @@ POST DBS_URI/register ```bash npm install +<<<<<<< HEAD ``` ## Run @@ -189,6 +190,11 @@ export ACCEPTED_PAYMENTS=ethereum,matic export NODE_RPC_URIS=default,default export BUNDLR_URI="https://node1.bundlr.network" #export BUNDLR_URI="https://devnet.bundlr.network" # Use Budnlr devnet when interacting with testnets +======= +export ACCEPTED_PAYMENTS=ethereum,matic,boba,boba-eth +export BUNDLR_URI="https://node1.bundlr.network" +#export BUNDLR_URI="https://devnet.bundlr.network" +>>>>>>> 03f4b2f (Partially Resolves #19) export PORT=8081 export PRIVATE_KEY="0000000000000000000000000000000000000000000000000000000000000000" export SQLITE_DB_PATH=/path/to/db/file @@ -262,6 +268,7 @@ curl -d '{ "type":"arweave", "userAddress": "0x000000000000000000000000000000000 curl -d '{ "quoteId":"60f7d48ccd08653b2ef2edfe4bbe4620", "signature": "0x0000000000000000000000000000000000000000", "files": ["https://example.com/", "ipfs://xxx"], "nonce": 0 }' -X POST -H 'Content-Type: application/json' http://localhost:8081/upload curl -d '{ "type":"arweave", "userAddress": "0x0000000000000000000000000000000000000000", "files": [{"length": 1256}, {"length": 5969}], "payment": {"chainId": 80001, "tokenAddress": "0x0000000000000000000000000000000000001010"} }' -X POST -H 'Content-Type: application/json' http://localhost:8081/getQuote +<<<<<<< HEAD curl -d '{ "quoteId":"40acc6937e1bd98631f47e7cbda72920", "signature": "0x0000000000000000000000000000000000000000", "files": ["https://example.com/", "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"], "nonce": 0 }' -X POST -H 'Content-Type: application/json' http://localhost:8081/upload curl 'http://localhost:8081/getStatus?quoteId=40acc6937e1bd98631f47e7cbda72920' curl 'http://localhost:8081/getLink?quoteId=40acc6937e1bd98631f47e7cbda72920&signature=0x0000000000000000000000000000000000000000&nonce=0' @@ -287,3 +294,7 @@ npm start curl -d '{ "type":"arweave", "userAddress": "0x0000000000000000000000000000000000000000", "files": [{"length": 100}], "payment": {"chainId": 137, "tokenAddress": "0x0000000000000000000000000000000000000000"} }' -X POST -H 'Content-Type: application/json' http://localhost:8081/getQuote ``` >>>>>>> 1988b4e (Framework + getQuote endpoint) +======= +curl -d '{ "quoteId":"047a6425546f8e9023e8af0ab47ba99f", "signature": "0x0000000000000000000000000000000000000000", "files": ["https://example.com/", "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"], "nonce": 0 }' -X POST -H 'Content-Type: application/json' http://localhost:8081/upload +``` +>>>>>>> 03f4b2f (Partially Resolves #19) diff --git a/app/controllers/quote.controller.js b/app/controllers/quote.controller.js index 02d652a..9b8b79f 100644 --- a/app/controllers/quote.controller.js +++ b/app/controllers/quote.controller.js @@ -1,6 +1,9 @@ const Bundlr = require("@bundlr-network/client"); const crypto = require("crypto"); <<<<<<< HEAD +<<<<<<< HEAD +======= +>>>>>>> 03f4b2f (Partially Resolves #19) const ethers = require('ethers'); const Quote = require("../models/quote.model.js"); @@ -581,10 +584,37 @@ exports.getLink = async (req, res) => { return; } +<<<<<<< HEAD const bundlr = new Bundlr.default(process.env.ARWEAVE_GATEWAY_URI, paymentToken, process.env.PRIVATE_KEY); const priceWei = await bundlr.getPrice(totalLength); const price = bundlr.utils.unitConverter(priceWei); +======= + let bundlr; + try { + bundlr = new Bundlr.default(process.env.BUNDLR_URI, paymentToken.name, process.env.PRIVATE_KEY, paymentToken.providerUrl ? {providerUrl: paymentToken.providerUrl, contractAddress: paymentToken.tokenAddress} : {}); + } + catch(err) { + res.status(500).send({ + message: err.message + }); + return; + } + + let priceWei; + try { + priceWei = await bundlr.getPrice(totalLength); + priceWei = ethers.BigNumber.from(priceWei.toString()); // need to convert so we can add buffer + } + catch(err) { + res.status(500).send({ + message: err.message + }); + return; + } + + const tokenAmount = priceWei.add(priceWei.div(10)); // add 10% buffer since prices fluctuate +>>>>>>> 03f4b2f (Partially Resolves #19) const quoteId = crypto.randomBytes(16).toString("hex"); const data = { @@ -595,6 +625,83 @@ exports.getLink = async (req, res) => { "quoteId": quoteId }; +<<<<<<< HEAD res.send(data); >>>>>>> 1988b4e (Framework + getQuote endpoint) }; +======= + // save data in database + const quote = new Quote({ + quoteId: quoteId, + status: Quote.QUOTE_STATUS_WAITING, + created: Date.now(), + chainId: chainId, + tokenAddress: tokenAddress, + userAddress: userAddress, + tokenAmount: tokenAmount.toString(), + approveAddress: "0x0000000000000000000000000000000000000000", // TODO: replace with real address + files: file_lengths + + }); + + // Save Reading in the database + Quote.create(quote, (err, data) => { + if(err) { + res.status(500).send({ + message: + err.message || "Error occurred while creating the quote." + }); + } + else { + // send receipt for data + res.send(data); + } + }); +}; + +exports.status = async (req, res) => { + const quoteidRegex = /^[a-fA-F0-9]{32}$/; + + if(!req.query || !req.query.quoteId) { + res.status(400).send({ + message: "Error, quoteId required." + }); + return; + } + const quoteId = req.query.quoteId; + + if(!quoteidRegex.test(quoteId)) { + res.status(400).send({ + message: "Invalid quoteId format." + }); + return; + } + + Quote.getStatus(quoteId, (err, data) => { + if(err) { + if(err.code == 404) { + res.status(404).send({ + status: 0 + }); + return; + } + res.status(500).send({ + message: + err.message || "Error occurred while looking up status." + }); + } + else { + // send receipt for data + res.send(data); + } + }); +}; + +exports.setStatus = async (quoteId, status) => { + Quote.setStatus(quoteId, status, (err, data) => { + if(err) { + console.log(err); + } + }); +}; +>>>>>>> 03f4b2f (Partially Resolves #19) diff --git a/app/controllers/tokens.js b/app/controllers/tokens.js index 5020b9d..d9df39a 100644 --- a/app/controllers/tokens.js +++ b/app/controllers/tokens.js @@ -2,6 +2,7 @@ <<<<<<< HEAD // TODO: Update `confirms` fields const tokens =[ +<<<<<<< HEAD // Mainnets, used with public Bundlr URIs. See for details https://docs.bundlr.network/docs/bundlers {bundlrName: "ethereum", chainId: 1, symbol: "ETH", providerUrl: "https://cloudflare-eth.com/", tokenAddress: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", confirms: 1}, {bundlrName: "matic", chainId: 137, symbol: "MATIC", providerUrl: "https://polygon-rpc.com", tokenAddress: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", confirms: 30}, @@ -12,6 +13,17 @@ const tokens =[ // Testnets, used with devnet Bundlr URI. See for details: https://docs.bundlr.network/docs/devnet {bundlrName: "matic", chainId: 80001, symbol: "MATIC", providerUrl: "https://rpc-mumbai.maticvigil.com/", tokenAddress: "0x9c3C9283D3e44854697Cd22D3Faa240Cfb032889", confirms: 1}, {bundlrName: "ethereum", chainId: 5, symbol: "ETH", providerUrl: "https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161", tokenAddress: "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6", confirms: 1}, +======= + {name: "ethereum", chainId: 1, tokenAddress: "0x0000000000000000000000000000000000000000", symbol: "ETH"}, + {name: "matic", chainId: 137, tokenAddress: "0x0000000000000000000000000000000000000000", symbol: "MATIC"}, + {name: "bnb", chainId: 56, tokenAddress: "0x0000000000000000000000000000000000000000", symbol: "BNB"}, + {name: "arbitrum", chainId: 42161, tokenAddress: "0x0000000000000000000000000000000000000000", symbol: "ETH"}, + {name: "avalanche", chainId: 43114, tokenAddress: "0x0000000000000000000000000000000000000000", symbol: "AVAX"}, + {name: "boba", chainId: 288, tokenAddress: "0xa18bF3994C0Cc6E3b63ac420308E5383f53120D7", symbol: "ETH"}, + {name: "boba-eth", chainId: 288, tokenAddress: "0x0000000000000000000000000000000000000000", symbol: "BOBA"}, + + {name: "matic", chainId: 80001, tokenAddress: "0x0000000000000000000000000000000000001010", providerUrl: "https://rpc-mumbai.maticvigil.com/"} +>>>>>>> 03f4b2f (Partially Resolves #19) ]; getToken = (chainId, tokenAddress) => { diff --git a/app/controllers/upload.controller.js b/app/controllers/upload.controller.js index a750503..8cdbee9 100644 --- a/app/controllers/upload.controller.js +++ b/app/controllers/upload.controller.js @@ -3,11 +3,16 @@ const Bundlr = require("@bundlr-network/client"); const axios = require('axios'); const File = require("../models/upload.model.js"); const Quote = require("../models/quote.model.js"); +<<<<<<< HEAD const Nonce = require("../models/nonce.model.js"); const ethers = require('ethers'); const { getToken } = require("./tokens.js"); const { errorResponse } = require("./error.js"); const { gasEstimate } = require("./gasEstimate.js"); +======= +const ethers = require('ethers'); +const { acceptToken } = require("./tokens.js"); +>>>>>>> 03f4b2f (Partially Resolves #19) exports.upload = async (req, res) => { console.log(`upload request: ${JSON.stringify(req.body)}`) @@ -29,6 +34,37 @@ exports.upload = async (req, res) => { return; } +<<<<<<< HEAD +======= + const nonce = req.body.nonce; + if(typeof nonce === "undefined") { + res.status(400).send({ + message: "Missing nonce." + }); + return; + } + if(typeof nonce !== "number") { + res.status(400).send({ + message: "Invalid nonce." + }); + return; + } + + const signature = req.body.signature; + if(typeof signature === "undefined") { + res.status(400).send({ + message: "Missing signature." + }); + return; + } + if(typeof signature !== "string") { + res.status(400).send({ + message: "Invalid signature." + }); + return; + } + +>>>>>>> 03f4b2f (Partially Resolves #19) const files = req.body.files; if(typeof files === "undefined") { errorResponse(req, res, null, 400, "Missing files field."); @@ -51,6 +87,7 @@ exports.upload = async (req, res) => { const cidRegex = /^(Qm[1-9A-HJ-NP-Za-km-z]{44,}|b[A-Za-z2-7]{58,}|B[A-Z2-7]{58,}|z[1-9A-HJ-NP-Za-km-z]{48,}|F[0-9A-F]{50,})$/i; for(let i = 0; i < files.length; i++) { if(typeof files[i] !== "string") { +<<<<<<< HEAD errorResponse(req, res, null, 400, `Invalid files field on index ${i}.`); return; } @@ -61,6 +98,18 @@ exports.upload = async (req, res) => { } if(!cidRegex.test(files[i].substring(7))) { errorResponse(req, res, null, 400, `Invalid CID on index ${i}.`); +======= + res.status(400).send({ + message: `Invalid files field on index ${i}.` + }); + return; + } + // TODO: validate URL format better + if(!files[i].startsWith('http://') && !files[i].startsWith('https://') && !files[i].startsWith('ipfs://')) { + res.status(400).send({ + message: `Invalid files URI on index ${i}.` + }); +>>>>>>> 03f4b2f (Partially Resolves #19) return; } } @@ -110,10 +159,24 @@ exports.upload = async (req, res) => { return; } +<<<<<<< HEAD if(signerAddress != userAddress) { errorResponse(req, res, null, 403, "Invalid signature."); return; } +======= + // check if new price is sufficient + let bundlr; + try { + bundlr = new Bundlr.default(process.env.BUNDLR_URI, paymentToken.name, process.env.PRIVATE_KEY, paymentToken.providerUrl ? {providerUrl: paymentToken.providerUrl, contractAddress: paymentToken.tokenAddress} : {}); + } + catch(err) { + res.status(500).send({ + message: err.message + }); + return; + } +>>>>>>> 03f4b2f (Partially Resolves #19) let oldNonce; try { @@ -135,10 +198,30 @@ exports.upload = async (req, res) => { return; } +<<<<<<< HEAD // check status of quote if(quote.status != Quote.QUOTE_STATUS_WAITING) { if(quote.status == Quote.QUOTE_STATUS_UPLOAD_END) { errorResponse(req, res, null, 400, "Quote has been completed."); +======= + let priceWei; + try { + priceWei = await bundlr.getPrice(quote.size); + } + catch(err) { + res.status(500).send({ + message: err.message + }); + return; + } + + const quoteTokenAmount = ethers.BigNumber.from(quote.tokenAmount); + + if(priceWei.gte(quoteTokenAmount)) { + res.status(402).send({ + message: `Quoted tokenAmount is less than current rate. Quoted amount: ${quote.tokenAmount}, current rate: ${tokenAmount}` + }); +>>>>>>> 03f4b2f (Partially Resolves #19) return; } else { @@ -310,6 +393,7 @@ exports.upload = async (req, res) => { return; } +<<<<<<< HEAD // Pull payment from user's account using transferFrom(userAddress, amount) const confirms = paymentToken.confirms; try { @@ -317,10 +401,18 @@ exports.upload = async (req, res) => { } catch(err) { console.error(`Error occurred while pulling payment from user address: ${err?.name}: ${err?.message}`); +======= + // TODO: Pull WETH from user's account into our EOA using transferFrom(userAddress, amount) + // TODO: Unwrap WETH to ETH + + // Fund our EOA's Bundlr Account + // TODO: Check the balance first +>>>>>>> 03f4b2f (Partially Resolves #19) try { Quote.setStatus(quoteId, Quote.QUOTE_STATUS_PAYMENT_PULL_FAILED); } catch(err) { +<<<<<<< HEAD console.error(`Error occurred while setting status to Quote.QUOTE_STATUS_PAYMENT_PULL_FAILED: ${err?.name}: ${err?.message}`); } return; @@ -368,6 +460,11 @@ exports.upload = async (req, res) => { } catch(err) { console.error(`Error occurred while setting status to Quote.QUOTE_STATUS_PAYMENT_PUSH_FAILED: ${err?.name}: ${err?.message}}`); +======= + // can't fund the quote + console.log("Can't fund the quote.") + console.log(err.message); +>>>>>>> 03f4b2f (Partially Resolves #19) return; } } @@ -463,11 +560,71 @@ exports.upload = async (req, res) => { reject(Quote.QUOTE_STATUS_UPLOAD_UPLOAD_FAILED); return; } +<<<<<<< HEAD }) .catch(err => { console.error(`Error occurred while downloading file ${file}, index ${index}: ${err?.name}: ${err?.message}`); reject(Quote.QUOTE_STATUS_UPLOAD_DOWNLOAD_FAILED); return; +======= + //console.log(`Quote index: ${index}, Qoute length: ${quotedFile.length}`); + + // download file + await axios({ + method: "get", + url: file, + responseType: "arraybuffer" + }) + .then(response => { + // download started + const contentType = response.headers['content-type']; + const httpLength = parseInt(response.headers['content-length']); + + if(httpLength) { + if(httpLength != quotedFile.length) { + // quoted size is different than real size + console.log(`Different lengths, quoted length = ${quotedFile.length}, http length ${httpLength}`); + } + } + + let tags = []; + if(contentType) { + // TODO: sanitize contentType + tags = [{name: "Content-Type", value: contentType}]; + } + + const uploader = bundlr.uploader.chunkedUploader; + + uploader.setChunkSize(524288); + uploader.setBatchSize(1); + + uploader.on("chunkUpload", (chunkInfo) => { + //console.log(`Uploaded Chunk number ${chunkInfo.id}, offset of ${chunkInfo.offset}, size ${chunkInfo.size} Bytes, with a total of ${chunkInfo.totalUploaded} bytes uploaded.`); + }); + uploader.on("chunkError", (e) => { + //console.error(`Error uploading chunk number ${e.id} - ${e.res.statusText}`); + }); + uploader.on("done", (finishRes) => { + //console.log(`Upload completed with ID ${finishRes.data.id}`); + Upload.setHash(quoteId, index, finishRes.data.id); + // TODO: HEAD request to Arweave Gateway to verify that file uploaded successfully + Quote.setStatus(quoteId, Quote.QUOTE_STATUS_UPLOAD_END); + }); + + const transactionOptions = {tags: tags}; + try { + // start upload + uploader.uploadData(Buffer.from(response.data, "binary"), transactionOptions); + // TODO: also hash the file + } + catch(error) { + console.error(error.message); + } + }) + .catch(error => { + console.log(error); + }); +>>>>>>> 03f4b2f (Partially Resolves #19) }); }); })).then(() => { diff --git a/package-lock.json b/package-lock.json index dd87902..23cfd04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,18 +7,25 @@ "": { "name": "arweave-upload", "version": "1.0.0", +<<<<<<< HEAD <<<<<<< HEAD "license": "Apache-2.0", ======= "license": "ISC", >>>>>>> 1988b4e (Framework + getQuote endpoint) +======= + "license": "Apache-2.0", +>>>>>>> 03f4b2f (Partially Resolves #19) "dependencies": { "@bundlr-network/client": "^0.8.9", "@types/express": "^4.17.11", "@types/qs": "^6.9.4", "axios": "^0.21.1", +<<<<<<< HEAD <<<<<<< HEAD "better-sqlite3": "^7.6.2", +======= +>>>>>>> 03f4b2f (Partially Resolves #19) "body-parser": "^1.19.0", "ethers": "^5.7.1", "express": "^4.17.1", diff --git a/package.json b/package.json index f3d6e60..c525c08 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,9 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [ + "arweave", "nodejs", "express", - "mysql", "restapi" ], "author": "David Hunt-Mateo & Corrie Sloot",