Merge pull request #1 from Rushilwiz/dev

This commit is contained in:
Rushil Umaretiya 2022-04-17 10:19:16 -04:00 committed by GitHub
commit b96993f73f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 6141 additions and 28 deletions

BIN
.github/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

4
.gitignore vendored
View File

@ -1,3 +1,5 @@
.vscode
# Django # # Django #
*.log *.log
*.pot *.pot
@ -139,4 +141,4 @@ GitHub.sublime-settings
#NextJS #NextJS
.next/ .next/
node_modules/ node_modules/
.DS_Store .DS_Store

44
README.md Normal file
View File

@ -0,0 +1,44 @@
<p align="center">
<a href="https://github.com/rushilwiz/rounded">
<img src=".github/logo.png" alt="Logo" width="480px" height="320px">
</a>
<h1 align="center">rounded.</h1>
</p>
![GitHub language](https://img.shields.io/github/languages/top/rushilwiz/rounded?color=FF6663)
![GitHub language count](https://img.shields.io/github/languages/count/rushilwiz/rounded?color=FEB144)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/rushilwiz/rounded?color=FAFD7B)
![GitHub repo size](https://img.shields.io/github/repo-size/rushilwiz/rounded?color=9EE09E)
![GitHub](https://img.shields.io/github/license/rushilwiz/rounded?color=9EC1CF)
![GitHub last commit](https://img.shields.io/github/last-commit/rushilwiz/rounded?color=CC99C9)
---
## Inspiration
Have you ever gone shopping, and after your big order, you were asked if you wanted to round up for charity? No matter how much you were buying, it was always a mere fraction of what you were paying, and it looked quite nice on your credit card bill. It's so easy for individuals, who wouldn't normally be donating, to take these pennies from their account and send it towards some real good, but we found that there was no real analog for this in the digital realm. As our day-to-day lives begin to move online, shouldn't our goodwill as well?
## What it does
Rounded is a Web3 platform linked to the DeSo blockchain and your finances. First, rounded uses banking APIs like Plaid and PayPal to connect to your online money platforms, and tracks your transaction history. Every time a new transaction is made (i.e. your PayPal balance changes), rounded will ask you if you'd like to round up. When you say yes, it will submit an order with the rounded price and a foundation of your choosing to our backend, which will take $DESO from your wallet and move it into the foundations' wallet. You will then be returned with a TransactionHex as a receipt for your order.
## How we built it
Our frontend was built from the ground up with NextJS and React, creating a comprehensive user interface that held all banking information client-side. This meant that sensitive user data was only being stored clientside, allowing our backend to handle pure order requests. The backend was built on Django and used the DRF (djangorestframework) to create a REST API to handle all requests and talk to the Postgre database. When making transactions on the $DESO blockchain, the backend uses the BitClout V1 DeSo API to generate and sign transactions.
## Challenges we ran into
Unfortunately, $DESO coin is still in Beta, and strong documentation is extremely scarce. We had great lengths of difficulty figuring out how to generate, authenticate, sign, and submit these transactions directly to the blockchain through DeSo's relatively new API, and since none of us had significant experience with controlling crypto wallets with code, it proved a pretty tough feat.
## Accomplishments that we're proud of
We're really happy with how the demo website came out, and eventually, we were able to overcome our issues with trading $DESO, and were able to generate unsigned transactions for demonstration purposes. This project was the culmination of a lot of quick thinking, documentation reading, and learning, and we're all proud of what we were able to take away from it.
## What we learned
Most profoundly was the structure of the $DESO blockchain. We learned about how to create, authenticate, and sign crypto trades on the blockchain, and about the BIP-39 mnemonic spec for generating secure wallets. Along with that, we learned a lot about JWT's and privileged API requests, whether it was to our own API or external ones. (nextjs does not like external API requests)
## What's next for rounded
Philanthropy is always going to be a global issue, and rounded is paving the path for it to move online.
The next steps for rounded would look like the development of a mobile dashboard along with SMS push notifications every time a transaction is made, asking users if they'd be willing to round up. Along with that, the creation of a browser extension to detect if users are on certain base URLs and when they are making a purchase, ask if they'd like to simultaneously make a donation. We may also open the platform to creators, who, when being paid online, can register with rounded to receive tips through our platform.
Web3 is the future, we're here to make sure that **everyone** is along for the ride.
- rounded

5
client/.env.local Normal file
View File

@ -0,0 +1,5 @@
AUTH0_SECRET="cd30f2a7f4385cade972fe29aaccfece5ea3efb5482b1f8c1926894ab4f96e58"
AUTH0_BASE_URL="http://localhost:3000"
AUTH0_ISSUER_BASE_URL="dev-p2ee3ykl.us.auth0.com"
AUTH0_CLIENT_ID="b2YFWRCgpZHZFDSA8F1KN2Z05sGC8wgc"
AUTH0_CLIENT_SECRET="SCD6CEhPbf-_ls4SK8AohWY0FCPPLwD52hH5Brrbs1ujlH5bUtE2YEl0W6KLTYve"

0
client/next.config.js Normal file
View File

3051
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,8 +12,12 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@auth0/nextjs-auth0": "^1.7.0",
"@reduxjs/toolkit": "^1.8.1", "@reduxjs/toolkit": "^1.8.1",
"axios": "^0.26.1",
"firebase": "^9.6.11",
"next": "^12.1.5", "next": "^12.1.5",
"oauth": "^0.9.15",
"react": "^18.0.0", "react": "^18.0.0",
"react-dom": "^18.0.0", "react-dom": "^18.0.0",
"react-redux": "^7.2.8", "react-redux": "^7.2.8",

131
client/src/app/index.css Normal file
View File

@ -0,0 +1,131 @@
@import url("https://fonts.googleapis.com/css2?family=Comfortaa&family=Ubuntu&display=swap");
body {
overflow-x: hidden;
overflow-style: none;
}
.logo {
display: flex;
justify-content: center;
align-items: center;
width: 100vw;
height: 100vh;
background: #e61919;
animation: color 30s linear infinite;
user-select: none;
position: absolute;
z-index: 3;
}
.menu {
position: fixed;
z-index: 2;
width: 100vw;
height: 50px;
background: rgba(0, 0, 0, 0.85);
color: #ccc;
text-align: center;
line-height: 50px;
font: 300 32px Comfortaa;
}
a {
padding: 5px 10px;
border: 1px solid;
border-radius: 30px;
font-family: Comfortaa;
color: #f39;
transition: background 0.3s;
}
a:hover {
background: #f39;
color: #fff;
border: 0;
}
.logo {
font: 300 150px Comfortaa;
color: #fff;
cursor: default;
}
.hint {
position: fixed;
z-index: 4;
bottom: 10vh;
width: 100vw;
text-align: center;
color: rgba(255, 255, 255, 0.6);
font: 300 30px Comfortaa;
}
.content {
padding: 30px;
position: relative;
top: 20vh;
height: 200vh;
font: 16px Ubuntu;
}
@-moz-keyframes color {
0% {
background: #e61919;
}
25% {
background: #80e619;
}
50% {
background: #19e5e6;
}
75% {
background: #7f19e6;
}
100% {
background: #e61919;
}
}
@-webkit-keyframes color {
0% {
background: #e61919;
}
25% {
background: #80e619;
}
50% {
background: #19e5e6;
}
75% {
background: #7f19e6;
}
100% {
background: #e61919;
}
}
@-o-keyframes color {
0% {
background: #e61919;
}
25% {
background: #80e619;
}
50% {
background: #19e5e6;
}
75% {
background: #7f19e6;
}
100% {
background: #e61919;
}
}
@keyframes color {
0% {
background: #e61919;
}
25% {
background: #80e619;
}
50% {
background: #19e5e6;
}
75% {
background: #7f19e6;
}
100% {
background: #e61919;
}
}

View File

@ -0,0 +1,103 @@
import { useAuth } from "../lib/firebase/authContext";
import { useRouter } from "next/router";
import { useState } from "react";
// import axios from "axios";
export default function AuthComponent() {
const { signup, signIn } = useAuth();
const router = useRouter();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [registerUsername, setRegisterUsername] = useState("");
const [registerPassword, setRegisterPassword] = useState("");
async function handleRegisterSubmit(e) {
try {
const requestOptions = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: registerUsername,
password: registerPassword,
}),
};
const response = await fetch(
"http://localhost:8000/api/profile/create",
requestOptions
);
const dataa = await response.json();
console.log(dataa);
} catch (error) {
console.log(error);
}
}
async function handleSubmit(e) {
try {
console.log(username);
console.log(password);
const requestOptions = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
"username": username, //prettier-ignore
"password": password, //prettier-ignore
}),
};
const response = await fetch(
"http://localhost:8000/api/token/",
requestOptions
);
const data = await response.json();
console.log(data);
localStorage.setItem("token", data.access);
localStorage.setItem("username", username);
// const reqOps = {
// method: "GET",
// headers: {
// "Content-Type": "application/json",
// Authorization: `Bearer ${data.access}`,
// },
// };
// const resp = await fetch("http://localhost:8000/api/foundation/", reqOps);
// const data2 = await resp.json();
// console.log(data2);
router.push("/");
} catch (error) {
console.log(error);
}
}
return (
<>
<h1>Login</h1>
<input
placeholder="Username"
onChange={(e) => setUsername(e.target.value)}
/>
<input
placeholder="Password"
type="password"
onChange={(e) => setPassword(e.target.value)}
/>
<button onClick={handleSubmit}>Submit</button>
<br />
<h1>OR</h1>
<br />
<h1>Register</h1>
<input
placeholder="Username"
onChange={(e) => setRegisterUsername(e.target.value)}
/>
<input
placeholder="Password"
type="password"
onChange={(e) => setRegisterPassword(e.target.value)}
/>
<button onClick={handleRegisterSubmit}>Submit</button>
</>
);
}

View File

@ -0,0 +1,126 @@
import { useEffect } from "react";
import axios from "axios";
export default function DashboardComponent({ clientID, secretID }) {
useEffect(() => {
const fetchData = async () => {
const bearerResponse = await fetch(
"https://api.sandbox.paypal.com/v1/oauth2/token",
{
method: "POST",
headers: {
Accept: "application/json",
"Accept-Language": "en_US",
"Content-Type": "application/x-www-form-urlencoded",
Authorization:
"Basic " +
btoa(
"AbA-5-a8nI5NbtAr0lrk2HNeJsc9u8W9TSy0zCWVgQKBkdkGTgRS5FRnpE2EuZge0Wvgzj0nRcgQAayY:EI_hNPlvttB2d8vIpqZC-qhuBGjZW_UOcFRJVs2-Axt8QFh1ZZ6j-jk0XYfrpOk7qgHPV7TubhgmuR3k"
),
},
body: "grant_type=client_credentials",
}
);
const bearer = await bearerResponse.json();
const access_token = bearer.access_token;
console.log(access_token);
var today = new Date();
var date =
today.getFullYear() +
"-" +
(today.getMonth() + 1) +
"-" +
today.getDate();
var time =
today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds();
var dateTime = date + "T" + time + "-07:00";
const balanceResponse = await fetch(
`https://api.sandbox.paypal.com/v1/reporting/balances?currency_code=ALL&as_of_time=${dateTime}`,
{
method: "GET",
headers: {
Accept: "application/json",
"Accept-Language": "en_US",
"Content-Type": "application/json",
Authorization: "Bearer " + access_token,
},
}
);
const balance = await balanceResponse.json();
console.log(balance);
};
fetchData();
}, []);
let orders = [
{
id: "1",
name: "Order 1",
date: "2020-01-01",
amount: "100.06",
vendor: "Amazon",
},
{
id: "2",
name: "Order 2",
date: "2020-02-25",
amount: "235.21",
vendor: "Walmart",
},
{
id: "3",
name: "Order 3",
date: "2020-04-15",
amount: "10.89",
vendor: "Ebxay",
},
];
return (
<div classNameName="relative overflow-x-auto shadow-md sm:rounded-lg">
<table className="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" className="px-6 py-3">
Order Name
</th>
<th scope="col" className="px-6 py-3">
Date
</th>
<th scope="col" className="px-6 py-3">
Vendor
</th>
<th scope="col" className="px-6 py-3">
Price
</th>
<th scope="col" className="px-6 py-3">
<span className="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody>
{orders.map((order) => (
<tr className="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<th
scope="row"
className="px-6 py-4 font-medium text-gray-900 dark:text-white whitespace-nowrap"
>
{order.name}
</th>
<td className="px-6 py-4">{order.date}</td>
<td className="px-6 py-4">{order.vendor}</td>
<td className="px-6 py-4">${order.amount}</td>
<td className="px-6 py-4 text-right">
<a
href="#"
className="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
Round up for charity?
</a>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

View File

@ -1,5 +1,7 @@
import "../app/globals.css"; import "../app/globals.css";
import "../app/index.css";
// import { AuthProvider } from "../common/lib/firebase/authContext";
export default function MyApp({ Component, pageProps }) { export default function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />; return <Component {...pageProps} />;
} }

8
client/src/pages/auth.js Normal file
View File

@ -0,0 +1,8 @@
import AuthComponent from "../common/components/Auth";
export default function AuthPage() {
return (
<div>
<AuthComponent />
</div>
);
}

View File

@ -0,0 +1,12 @@
import DashboardComponent from "../common/components/Dashboard/Dashboard";
export default function Dashboard() {
return (
<div>
<h1>This is the dashboard page.</h1>
<DashboardComponent
clientID="AbA-5-a8nI5NbtAr0lrk2HNeJsc9u8W9TSy0zCWVgQKBkdkGTgRS5FRnpE2EuZge0Wvgzj0nRcgQAayY"
secretID="EI_hNPlvttB2d8vIpqZC-qhuBGjZW_UOcFRJVs2-Axt8QFh1ZZ6j-jk0XYfrpOk7qgHPV7TubhgmuR3k"
/>
</div>
);
}

View File

@ -0,0 +1,28 @@
import { useEffect, useState } from "react";
export default function DonatePage() {
const [foundation, setFoundation] = useState([]);
useEffect(() => {
async function getData() {
const reqOps = {
method: "GET",
headers: {
"Content-Type": "application/json",
},
};
const resp = await fetch("http://localhost:8000/api/foundation/", reqOps);
const data = await resp.json();
// console.log(data);
setFoundation(JSON.stringify(data));
console.log(foundation);
}
// const d = ;
getData();
// setFoundation(d);
}, []);
return (
<div>
<h1>This is the donate page.</h1>
<p>This page is under construction. Please check back later.</p>
</div>
);
}

View File

@ -1,7 +1,38 @@
// import { useAuth } from "../common/lib/firebase/authContext";
import { useEffect, useState } from "react";
export default function HomePage() { export default function HomePage() {
// const { currentUser } = useAuth();
const [currentUser, setCurrentUser] = useState(null);
useEffect(() => {
// Perform localStorage action
setCurrentUser(localStorage.getItem("username"));
}, []);
return ( return (
<div> <div>
<div
className="logo"
data-0="opacity: 1; display: flex"
data-99="opacity: 0; display: none"
data-100="display: none"
>
<div className="logo">rounded.</div>
</div>
<div className="hint" data-0="bottom: 10vh" data-100="bottom: -25px">
the future of rounding up, for a better tomorrow
</div>
<div className="menu">rounded.</div>
<div className="content">We're rounding up where you can't.</div>
<h1>This is HackTJ 2022!</h1> <h1>This is HackTJ 2022!</h1>
{currentUser ? (
<p>You're logged in! Current username: {currentUser}</p>
) : (
<p>Not logged in</p>
)}
<a href="/dashboard">Go to Dashboard</a>
</div> </div>
); );
} }

View File

@ -0,0 +1,37 @@
import { useState } from "react";
export default function Profile() {
const [wallet, setWallet] = useState("");
async function handleSubmit(e) {
try {
const reqOps = {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${localStorage.getItem("token")}`, //prettier-ignore
},
body: {
"address": wallet, //prettier-ignore
},
};
const resp = await fetch(
"http://localhost:8000/api/profile/add_wallet",
reqOps
);
const data = await resp.json();
console.log(data);
} catch (error) {
console.log(error);
}
}
return (
<div>
<h1>Profile</h1>
<h2>Add Wallet</h2>
<input
placeholder="Address"
onChange={(e) => setWallet(e.target.value)}
/>
<button onClick={handleSubmit}>Submit</button>
</div>
);
}

View File

@ -1,8 +1,5 @@
module.exports = { module.exports = {
content: [ content: ["./src/**/*.{html,js}"],
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: { theme: {
extend: {}, extend: {},
}, },

1987
client/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

18
server/Pipfile Normal file
View File

@ -0,0 +1,18 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
django = "*"
gunicorn = "*"
python-dotenv = "*"
djangorestframework = "*"
django-cors-headers = "*"
djangorestframework-simplejwt = "*"
requests = "*"
[dev-packages]
[requires]
python_version = "3.9"

147
server/Pipfile.lock generated Normal file
View File

@ -0,0 +1,147 @@
{
"_meta": {
"hash": {
"sha256": "edc0e33714e869ceb2bf072793d154b82545e005bf908ca2949645b193c13297"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.9"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"asgiref": {
"hashes": [
"sha256:2f8abc20f7248433085eda803936d98992f1343ddb022065779f37c5da0181d0",
"sha256:88d59c13d634dcffe0510be048210188edd79aeccb6a6c9028cdad6f31d730a9"
],
"markers": "python_version >= '3.7'",
"version": "==3.5.0"
},
"certifi": {
"hashes": [
"sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
"sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
],
"version": "==2021.10.8"
},
"charset-normalizer": {
"hashes": [
"sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
"sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
],
"markers": "python_version >= '3'",
"version": "==2.0.12"
},
"django": {
"hashes": [
"sha256:07c8638e7a7f548dc0acaaa7825d84b7bd42b10e8d22268b3d572946f1e9b687",
"sha256:4e8177858524417563cc0430f29ea249946d831eacb0068a1455686587df40b5"
],
"index": "pypi",
"version": "==4.0.4"
},
"django-cors-headers": {
"hashes": [
"sha256:a22be2befd4069c4fc174f11cf067351df5c061a3a5f94a01650b4e928b0372b",
"sha256:eb98389bf7a2afc5d374806af4a9149697e3a6955b5a2dc2bf049f7d33647456"
],
"index": "pypi",
"version": "==3.11.0"
},
"djangorestframework": {
"hashes": [
"sha256:0c33407ce23acc68eca2a6e46424b008c9c02eceb8cf18581921d0092bc1f2ee",
"sha256:24c4bf58ed7e85d1fe4ba250ab2da926d263cd57d64b03e8dcef0ac683f8b1aa"
],
"index": "pypi",
"version": "==3.13.1"
},
"djangorestframework-simplejwt": {
"hashes": [
"sha256:75323528a7b910843b879194746f0ebc3481c4ad9f354de2dd162160662fb95a",
"sha256:cd3ee3aabdc98de104957c136e74ba2a16571b151c88ecd97421fce171a03ea4"
],
"index": "pypi",
"version": "==5.1.0"
},
"gunicorn": {
"hashes": [
"sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e",
"sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"
],
"index": "pypi",
"version": "==20.1.0"
},
"idna": {
"hashes": [
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
],
"markers": "python_version >= '3'",
"version": "==3.3"
},
"pyjwt": {
"hashes": [
"sha256:b888b4d56f06f6dcd777210c334e69c737be74755d3e5e9ee3fe67dc18a0ee41",
"sha256:e0c4bb8d9f0af0c7f5b1ec4c5036309617d03d56932877f2f7a0beeb5318322f"
],
"markers": "python_version >= '3.6'",
"version": "==2.3.0"
},
"python-dotenv": {
"hashes": [
"sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f",
"sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"
],
"index": "pypi",
"version": "==0.20.0"
},
"pytz": {
"hashes": [
"sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7",
"sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"
],
"version": "==2022.1"
},
"requests": {
"hashes": [
"sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61",
"sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"
],
"index": "pypi",
"version": "==2.27.1"
},
"sqlparse": {
"hashes": [
"sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae",
"sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"
],
"markers": "python_version >= '3.5'",
"version": "==0.4.2"
},
"tzdata": {
"hashes": [
"sha256:238e70234214138ed7b4e8a0fab0e5e13872edab3be586ab8198c407620e2ab9",
"sha256:8b536a8ec63dc0751342b3984193a3118f8fca2afe25752bb9b7fffd398552d3"
],
"markers": "sys_platform == 'win32'",
"version": "==2022.1"
},
"urllib3": {
"hashes": [
"sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14",
"sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==1.26.9"
}
},
"develop": {}
}

0
server/api/__init__.py Normal file
View File

9
server/api/admin.py Normal file
View File

@ -0,0 +1,9 @@
from django.contrib import admin
from . import models
# Register your models here.
admin.site.register(models.Consumer)
admin.site.register(models.Foundation)
admin.site.register(models.Wallet)
admin.site.register(models.FoundationOrder)

6
server/api/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api'

View File

@ -0,0 +1,67 @@
# Generated by Django 4.0.4 on 2022-04-16 20:35
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Consumer',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('access_token', models.CharField(max_length=100)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name_plural': 'Consumers',
},
),
migrations.CreateModel(
name='Foundation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('description', models.TextField()),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name_plural': 'Foundations',
},
),
migrations.CreateModel(
name='Wallet',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('address', models.CharField(max_length=100)),
],
options={
'verbose_name_plural': 'Wallets',
},
),
migrations.CreateModel(
name='FoundationOrder',
fields=[
('price', models.DecimalField(decimal_places=2, max_digits=9)),
('uuid', models.UUIDField(primary_key=True, serialize=False, unique=True)),
('consumer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='foundation_orders', to='api.consumer')),
('foundation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='foundation_orders', to='api.foundation')),
],
options={
'verbose_name_plural': 'Foundation Orders',
},
),
migrations.AddField(
model_name='foundation',
name='wallet',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='api.wallet'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 4.0.4 on 2022-04-16 20:37
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('api', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='consumer',
name='wallet',
field=models.OneToOneField(default=None, on_delete=django.db.models.deletion.CASCADE, to='api.wallet'),
preserve_default=False,
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 4.0.4 on 2022-04-16 21:02
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('api', '0002_consumer_wallet'),
]
operations = [
migrations.AlterField(
model_name='consumer',
name='wallet',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.wallet'),
),
]

View File

@ -0,0 +1,32 @@
# Generated by Django 4.0.4 on 2022-04-17 13:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0003_alter_consumer_wallet'),
]
operations = [
migrations.RemoveField(
model_name='consumer',
name='access_token',
),
migrations.AddField(
model_name='foundation',
name='hex',
field=models.CharField(blank=True, max_length=200, null=True),
),
migrations.AlterField(
model_name='foundation',
name='description',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='foundation',
name='name',
field=models.CharField(blank=True, max_length=100, null=True),
),
]

View File

51
server/api/models.py Normal file
View File

@ -0,0 +1,51 @@
from django.contrib.auth.models import User
from django.db import models
# Create your models here.
class Wallet (models.Model):
address = models.CharField(max_length=100)
def __str__(self):
return self.address[0:4]
# return f'{self.consumer.user.username}\'s wallet'
class Meta:
verbose_name_plural = "Wallets"
class Consumer (models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
wallet = models.OneToOneField(Wallet, null=True, blank=True, on_delete=models.CASCADE)
phone = models.CharField(max_length=32, null=True, blank=True),
def __str__(self):
return f'{self.user.username}\'s profile'
class Meta:
verbose_name_plural = "Consumers"
class Foundation (models.Model):
wallet = models.OneToOneField(Wallet, on_delete=models.CASCADE)
user = models.OneToOneField(User, on_delete=models.CASCADE)
name = models.CharField(max_length=100, null=True, blank=True)
description = models.TextField(null=True, blank=True)
hex = models.CharField(max_length=200, null=True, blank=True)
def __str__(self):
return f'{self.user.username}\'s foundation'
class Meta:
verbose_name_plural = "Foundations"
class FoundationOrder (models.Model):
consumer = models.ForeignKey(Consumer, related_name='foundation_orders', on_delete=models.CASCADE)
foundation = models.ForeignKey(Foundation, related_name='foundation_orders', on_delete=models.CASCADE)
price = models.DecimalField(max_digits=9, decimal_places=2)
uuid = models.UUIDField(primary_key=True, unique=True)
class Meta:
verbose_name_plural = "Foundation Orders"
def __str__(self):
return f'{self.consumer.user.username}\'s Order to {self.foundation.name} for {self.price}'

62
server/api/serializers.py Normal file
View File

@ -0,0 +1,62 @@
from operator import mod
from django.contrib.auth.models import User
from rest_framework import serializers
from . import models
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('username', 'email', 'first_name', 'last_name')
class ConsumerCreateSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
def create(self, validated_data):
user = User.objects.create_user(**validated_data)
user.set_password(validated_data['password'])
user.save()
consumer = models.Consumer.objects.create(user=user)
consumer.save()
return user
class Meta:
model = models.User
fields = ('username', 'password', 'email', 'first_name', 'last_name')
class FoundationSerializer(serializers.ModelSerializer):
user = UserSerializer()
class Meta:
model = models.Foundation
fields = ('user', 'name', 'description')
class ConsumerOrderSerializer(serializers.ModelSerializer):
foundation = FoundationSerializer()
class Meta:
model = models.FoundationOrder
fields = ('price', 'uuid', 'foundation')
class ConsumerSerializer(serializers.ModelSerializer):
user = UserSerializer()
class Meta:
model = models.Consumer
fields = ('user', 'access_token', 'foundation_orders')
class FoundationOrderSerializer(serializers.ModelSerializer):
consumer = ConsumerSerializer()
class Meta:
model = models.FoundationOrder
fields = ('price', 'uuid', 'consumer')
class FoundationOrderCreateSerializer(serializers.ModelSerializer):
class Meta:
model = models.FoundationOrder
fields = ('price', 'uuid', 'foundation')

3
server/api/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

20
server/api/urls.py Normal file
View File

@ -0,0 +1,20 @@
from django.urls import path
from . import views
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
path('profile/create', views.ConsumerCreate.as_view()),
path('profile/add_wallet', views.ConsumerAddWallet.as_view()),
path('profile/update', views.ConsumerUpdate.as_view()),
path('profile/', views.ConsumerDetail.as_view()),
path('foundation/', views.FoundationViewSet.as_view({'get': 'list', 'post': 'create'})),
path('foundation/<int:pk>/', views.FoundationViewSet.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'})),
path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('generate_order/', views.generate_order),
]

77
server/api/views.py Normal file
View File

@ -0,0 +1,77 @@
from django.shortcuts import render
from uuid import uuid4
import requests
from .models import *
from .serializers import *
from rest_framework import status, permissions
from rest_framework.views import APIView
from rest_framework.viewsets import ModelViewSet
from rest_framework.generics import CreateAPIView
from rest_framework.response import Response
# Create your views here.
class FoundationViewSet(ModelViewSet):
permission_classes = [permissions.AllowAny]
queryset = models.Foundation.objects.all()
serializer_class = FoundationSerializer
class ConsumerOrderViewSet(ModelViewSet):
queryset = ''
serializerClass = ConsumerOrderSerializer
def list(self, request, *args, **kwargs):
queryset = request.user.consumer.foundation_orders.all()
serializer = ConsumerOrderSerializer(queryset, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
class ConsumerCreate(CreateAPIView):
model = User
permission_classes = [permissions.AllowAny]
serializer_class = ConsumerCreateSerializer
class ConsumerDetail(APIView):
def get(self, request, format=None):
profile = request.user.consumer
serializer = ConsumerSerializer(profile)
return Response(serializer.data, status=status.HTTP_200_OK)
class ConsumerUpdate(APIView):
def post(self, request, format=None):
profile = request.user.consumer
profile.phone = request.data['phone']
profile.save()
class ConsumerAddWallet(APIView):
def post(self, request, format=None):
profile = request.user.consumer
profile.wallet = Wallet.objects.create(address=request.data['address'])
profile.save()
def generate_order(request):
if request.method == 'POST':
foundation = User.objects.get(username=request.POST['foundation']).foundation
price = int(request.POST['price'])
uuid = uuid4()
order = FoundationOrder.objects.create(consumer=request.user.consumer, foundation=foundation, price=price, uuid=uuid)
rate = int(requests.get("https://node.deso.org/api/v0/get-exchange-rate").json()["USDCentsPerBitCloutExchangeRate"])
price_in_nanos = (((10**9) * price) / rate)
options = {
"SenderPublicKeyBase58Check": request.user.consumer.wallet.address,
"RecipientPublicKeyOrUsername": foundation.wallet.address,
"AmountNanos": price_in_nanos
}
hex = request.post('https://node.deso.org/api/v0/send-deso', json=options).json()["TransactionHex"]
foundation.hex = hex
foundation.save()

View File

@ -0,0 +1,2 @@
SECRET_KEY=notsosecret
DEBUG=True

View File

@ -12,6 +12,10 @@ https://docs.djangoproject.com/en/4.0/ref/settings/
from pathlib import Path from pathlib import Path
import os
from dotenv import load_dotenv
load_dotenv()
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
@ -20,26 +24,36 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-ioum!dn#uh5t3yn_15j)4qtjp6wyl24-xx*mv6d1kl&wk_-mx1' SECRET_KEY = str(os.getenv('SECRET_KEY'))
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = str(os.getenv("DEBUG")).lower() == "true"
ALLOWED_HOSTS = [] if DEBUG:
ALLOWED_HOSTS = ["*"]
else:
ALLOWED_HOSTS = ["api.roundedapp.tech"]
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'api',
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'corsheaders',
'rest_framework'
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
@ -68,16 +82,39 @@ TEMPLATES = [
] ]
WSGI_APPLICATION = 'config.wsgi.application' WSGI_APPLICATION = 'config.wsgi.application'
CORS_ORIGIN_ALLOW_ALL=True
# Database # Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases # https://docs.djangoproject.com/en/4.0/ref/settings/#databases
DATABASES = { if DEBUG:
'default': { DATABASES = {
'ENGINE': 'django.db.backends.sqlite3', 'default': {
'NAME': BASE_DIR / 'db.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
} }
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'rounded',
'USER': 'rounded',
'PASSWORD': 'rounded',
'HOST': 'localhost',
'PORT': '5432',
}
}
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
} }
@ -116,6 +153,8 @@ USE_TZ = True
# https://docs.djangoproject.com/en/4.0/howto/static-files/ # https://docs.djangoproject.com/en/4.0/howto/static-files/
STATIC_URL = 'static/' STATIC_URL = 'static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field

View File

@ -14,8 +14,9 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.contrib import admin from django.contrib import admin
from django.urls import path from django.urls import include, path
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('api/', include('api.urls')),
] ]