Merge pull request #7 from exstake/save-user-balances

Save user balances
This commit is contained in:
João Geonizeli
2021-08-11 20:53:23 -03:00
committed by GitHub
33 changed files with 937 additions and 25 deletions

View File

@@ -0,0 +1,8 @@
# frozen_string_literal: true
module Admin
class BalancesController < Admin::ApplicationController
def valid_action?(name, resource = resource_class)
["destroy"].exclude?(name.to_s) && super
end
end
end

View File

@@ -0,0 +1,8 @@
# frozen_string_literal: true
module Admin
class CurrenciesController < Admin::ApplicationController
def valid_action?(name, resource = resource_class)
["new", "edit", "destroy"].exclude?(name.to_s) && super
end
end
end

View File

@@ -1,7 +1,5 @@
# frozen_string_literal: true
class ApplicationController < ActionController::Base
include Pundit
before_action :configure_devise_permitted_parameters, if: :devise_controller?
protected

View File

@@ -1,5 +1,7 @@
# frozen_string_literal: true
class GraphqlController < ApplicationController
# protect_from_forgery with: :null_session
def execute
variables = prepare_variables(params[:variables])
query = params[:query]

View File

@@ -0,0 +1,54 @@
# frozen_string_literal: true
require "administrate/base_dashboard"
class BalanceDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
user: Field::BelongsTo,
currency: Field::BelongsTo,
id: Field::Number,
amount: Field::String.with_options(searchable: false),
created_at: Field::DateTime,
updated_at: Field::DateTime,
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = [:user, :currency, :id, :amount].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = [:user, :currency, :id, :amount, :created_at, :updated_at].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = [:user, :currency, :amount].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { resources.where(open: true) }
# }.freeze
COLLECTION_FILTERS = {}.freeze
# Overwrite this method to customize how balances are displayed
# across all pages of the admin dashboard.
#
# def display_resource(balance)
# "Balance ##{balance.id}"
# end
end

View File

@@ -0,0 +1,52 @@
# frozen_string_literal: true
require "administrate/base_dashboard"
class CurrencyDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
id: Field::Number,
name: Field::String,
created_at: Field::DateTime,
updated_at: Field::DateTime,
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = [:id, :name, :created_at, :updated_at].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = [:id, :name, :created_at, :updated_at].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = [:name].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { resources.where(open: true) }
# }.freeze
COLLECTION_FILTERS = {}.freeze
# Overwrite this method to customize how currencies are displayed
# across all pages of the admin dashboard.
#
def display_resource(currency)
currency.name
end
end

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true
module Types
class BalanceType < Types::BaseObject
implements GraphQL::Types::Relay::Node
global_id_field :id
graphql_name "Balance"
field :id, ID, null: false
field :currency, CurrencyType, null: false
field :amount, String, null: false
end
end

View File

@@ -0,0 +1,12 @@
# frozen_string_literal: true
module Types
class CurrencyType < Types::BaseObject
implements GraphQL::Types::Relay::Node
global_id_field :id
graphql_name "Currency"
field :id, ID, null: false
field :name, String, null: false
end
end

View File

@@ -8,5 +8,10 @@ module Types
def current_user
context[:current_user]
end
field :balances, BalanceType.connection_type, null: false
def balances
Pundit.policy_scope(current_user, Balance)
end
end
end

View File

@@ -2,9 +2,10 @@
module Types
class UserType < Types::BaseObject
# implements GraphQL::Types::Relay::Node
global_id_field :id
graphql_name "User"
field :id, ID, null: false
field :first_name, String, null: false
field :last_name, String, null: false

View File

@@ -4,15 +4,22 @@ class XStakeSchema < GraphQL::Schema
query(Types::QueryType)
def self.resolve_type(abstract_type, obj, ctx)
raise(GraphQL::RequiredImplementationMissingError)
case obj
when Currency
Types::CurrencyType
when Balance
Types::BalanceType
else
raise(GraphQL::RequiredImplementationMissingError, "Unexpected object: #{obj}")
end
end
def self.id_from_object(object, type_definition, query_ctx)
def self.id_from_object(object, type_definition, ctx)
GraphQL::Schema::UniqueWithinType.encode(type_definition.name, object.id)
end
def self.object_from_id(id, query_ctx)
def self.object_from_id(id, ctx)
type_name, item_id = GraphQL::Schema::UniqueWithinType.decode(id)
type_name.constantize.find(item_id)
Pundit.policy_scope(ctx[:current_user], type_name.constantize).find(item_id)
end
end

View File

@@ -1,5 +1,127 @@
type Balance implements Node {
amount: Float!
currency: Currency!
id: ID!
}
"""
The connection type for Balance.
"""
type BalanceConnection {
"""
A list of edges.
"""
edges: [BalanceEdge]
"""
A list of nodes.
"""
nodes: [Balance]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type BalanceEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: Balance
}
type Currency implements Node {
id: ID!
name: String!
}
"""
An object with an ID.
"""
interface Node {
"""
ID of the object.
"""
id: ID!
}
"""
Information about pagination in a connection.
"""
type PageInfo {
"""
When paginating forwards, the cursor to continue.
"""
endCursor: String
"""
When paginating forwards, are there more items?
"""
hasNextPage: Boolean!
"""
When paginating backwards, are there more items?
"""
hasPreviousPage: Boolean!
"""
When paginating backwards, the cursor to continue.
"""
startCursor: String
}
type Query {
balances(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): BalanceConnection!
currentUser: User
"""
Fetches an object given its ID.
"""
node(
"""
ID of the object.
"""
id: ID!
): Node
"""
Fetches a list of objects given a list of IDs.
"""
nodes(
"""
IDs of the objects.
"""
ids: [ID!]!
): [Node]!
}
type User {

View File

@@ -2,14 +2,17 @@ import type { FC } from "react";
import React from "react";
import { Switch, Route } from "react-router-dom";
import { Home } from "./pages";
import { Home, Wallet } from "./pages";
export const Routes: FC = () => {
return (
<Switch>
<Route path="/">
<Route exact path="/">
<Home />
</Route>
<Route exact path="/wallet">
<Wallet />
</Route>
</Switch>
);
};

View File

@@ -14,10 +14,6 @@ const MenuItems: MenuItem[] = [
label: "Início",
path: "/",
},
{
label: "Stake",
path: "/stake",
},
{
label: "Carteira",
path: "/wallet",

View File

@@ -0,0 +1,94 @@
import { graphql } from "babel-plugin-relay/macro";
import type { FC } from "react";
import React from "react";
import { useLazyLoadQuery } from "react-relay";
import { tokens } from "../../constants/pancake/Tokens";
import type { WalletQuery } from "./__generated__/WalletQuery.graphql";
export const Wallet: FC = () => {
const { balances } = useLazyLoadQuery<WalletQuery>(
graphql`
query WalletQuery {
balances {
nodes {
id
amount
currency {
name
}
}
}
}
`,
{}
);
const tokensList = Object.values(tokens);
return (
<div className="flex flex-col h-full w-full overflow-x-hidden mt-16">
<div className="container mx-auto px-4 sm:px-8 max-w-3xl">
<div className="py-8">
<div className="-mx-4 sm:-mx-8 px-4 sm:px-8 py-4 overflow-x-auto">
<div className="inline-block min-w-full shadow rounded-lg overflow-hidden">
<table className="min-w-full leading-normal">
<thead>
<tr>
<th
scope="col"
className="px-5 py-3 bg-white border-b border-gray-200 text-gray-800 text-left text-sm uppercase font-normal"
>
Moeda
</th>
<th
scope="col"
className="px-5 py-3 bg-white border-b border-gray-200 text-gray-800 text-left text-sm uppercase font-normal"
>
Saldo
</th>
</tr>
</thead>
<tbody>
{balances.nodes?.map((balance) => {
const token = tokensList.find(
({ symbol }) => symbol === balance?.currency.name
);
return (
<tr key={balance?.id}>
<td className="px-5 py-5 border-b border-gray-200 bg-white text-sm">
<div className="flex items-center">
<div className="flex-shrink-0">
<a href="/" className="block relative">
<img
alt="profil"
src={`https://pancakeswap.finance/images/tokens/${token?.address["56"]}.svg`}
className="mx-auto object-cover rounded-full h-10 w-10 "
/>
</a>
</div>
<div className="ml-3">
<p className="text-gray-900 whitespace-no-wrap">
{balance?.currency.name}
</p>
</div>
</div>
</td>
<td className="px-5 py-5 border-b border-gray-200 bg-white text-sm">
<p className="text-gray-900 whitespace-no-wrap">
{balance?.amount}
</p>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,165 @@
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from "relay-runtime";
export type WalletQueryVariables = {};
export type WalletQueryResponse = {
readonly balances: {
readonly nodes: ReadonlyArray<{
readonly id: string;
readonly amount: number;
readonly currency: {
readonly name: string;
};
} | null> | null;
};
};
export type WalletQuery = {
readonly response: WalletQueryResponse;
readonly variables: WalletQueryVariables;
};
/*
query WalletQuery {
balances {
nodes {
id
amount
currency {
name
id
}
}
}
}
*/
const node: ConcreteRequest = (function(){
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v1 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "amount",
"storageKey": null
},
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "WalletQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "BalanceConnection",
"kind": "LinkedField",
"name": "balances",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Balance",
"kind": "LinkedField",
"name": "nodes",
"plural": true,
"selections": [
(v0/*: any*/),
(v1/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "Currency",
"kind": "LinkedField",
"name": "currency",
"plural": false,
"selections": [
(v2/*: any*/)
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [],
"kind": "Operation",
"name": "WalletQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "BalanceConnection",
"kind": "LinkedField",
"name": "balances",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Balance",
"kind": "LinkedField",
"name": "nodes",
"plural": true,
"selections": [
(v0/*: any*/),
(v1/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "Currency",
"kind": "LinkedField",
"name": "currency",
"plural": false,
"selections": [
(v2/*: any*/),
(v0/*: any*/)
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "6b8d0c664bd2d9df4d323c19c4a823a5",
"id": null,
"metadata": {},
"name": "WalletQuery",
"operationKind": "query",
"text": "query WalletQuery {\n balances {\n nodes {\n id\n amount\n currency {\n name\n id\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = '428f4f1ab769f9056dd38ec641a30733';
export default node;

View File

@@ -0,0 +1 @@
export * from "./Wallet";

View File

@@ -1 +1,2 @@
export * from "./Home";
export * from "./Wallet";

View File

@@ -1 +0,0 @@
export * from "./Home";

29
app/models/balance.rb Normal file
View File

@@ -0,0 +1,29 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: balances
#
# id :bigint not null, primary key
# amount :decimal(20, 10) default(0.0), not null
# created_at :datetime not null
# updated_at :datetime not null
# currency_id :bigint not null
# user_id :bigint not null
#
# Indexes
#
# index_balances_on_currency_id (currency_id)
# index_balances_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (currency_id => currencies.id)
# fk_rails_... (user_id => users.id)
#
class Balance < ApplicationRecord
belongs_to :user
belongs_to :currency
validates :amount, presence: true
end

14
app/models/currency.rb Normal file
View File

@@ -0,0 +1,14 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: currencies
#
# id :bigint not null, primary key
# name :string not null
# created_at :datetime not null
# updated_at :datetime not null
#
class Currency < ApplicationRecord
validates :name, presence: true
end

View File

@@ -25,6 +25,7 @@ class User < ApplicationRecord
:recoverable, :rememberable, :validatable
has_many :documents, class_name: "UserDocument", dependent: :destroy
has_many :balances, dependent: :restrict_with_error
validates :first_name, :last_name, :email, presence: true
validates :email, uniqueness: true

View File

@@ -0,0 +1,10 @@
# frozen_string_literal: true
class BalancePolicy < ApplicationPolicy
class Scope < Scope
def resolve
return scope.none if user.nil?
scope.where(user_id: user.id)
end
end
end

View File

@@ -5,9 +5,16 @@ pt-BR:
one: Documentos de Usuário
other: Documentos de Usuários
admin_user:
one: Usuário Administrador
other: Usuários Administradores
one: Administrador
other: Administradores
currency:
one: Moeda
other: Moedas
balance:
one: Saldo
other: Saldos
attributes:
user:
first_name: Primeiro nome
last_name: Último nome
full_name: Nome completo

View File

@@ -5,7 +5,8 @@ Rails.application.routes.draw do
namespace :admin do
resources :users
resources :user_documents
resources :balances
resources :currencies
resources :admin_users
root to: "users#index"

View File

@@ -0,0 +1,10 @@
# frozen_string_literal: true
class CreateCurrencies < ActiveRecord::Migration[6.1]
def change
create_table(:currencies) do |t|
t.string(:name, null: false)
t.timestamps
end
end
end

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true
class CreateBalances < ActiveRecord::Migration[6.1]
def change
create_table(:balances) do |t|
t.references(:user, null: false, foreign_key: true)
t.references(:currency, null: false, foreign_key: true)
t.decimal(:amount, precision: 20, scale: 10, null: false, default: 0)
t.timestamps
end
end
end

20
db/schema.rb generated
View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2021_08_08_153626) do
ActiveRecord::Schema.define(version: 2021_08_11_121726) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -55,6 +55,22 @@ ActiveRecord::Schema.define(version: 2021_08_08_153626) do
t.index ["reset_password_token"], name: "index_admin_users_on_reset_password_token", unique: true
end
create_table "balances", force: :cascade do |t|
t.bigint "user_id", null: false
t.bigint "currency_id", null: false
t.decimal "amount", precision: 20, scale: 10, default: "0.0", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["currency_id"], name: "index_balances_on_currency_id"
t.index ["user_id"], name: "index_balances_on_user_id"
end
create_table "currencies", force: :cascade do |t|
t.string "name", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "user_documents", force: :cascade do |t|
t.string "status", null: false
t.bigint "user_id", null: false
@@ -79,5 +95,7 @@ ActiveRecord::Schema.define(version: 2021_08_08_153626) do
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "balances", "currencies"
add_foreign_key "balances", "users"
add_foreign_key "user_documents", "users"
end

View File

@@ -1,8 +1,216 @@
# frozen_string_literal: true
# This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
#
# Examples:
#
# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
# Character.create(name: 'Luke', movie: movies.first)
AdminUser.create(email: "admin@example.com", password: "password")
user = User.create!(
first_name: "Test",
last_name: "User",
email: "user@example.com",
password: "password"
)
tokens = [
"BNB",
"CAKE",
"CHESS",
"TITAN",
"ONE",
"MASK",
"DVI",
"ADX",
"BSCPAD",
"RABBIT",
"FORM",
"TXL",
"ORBS",
"COS",
"BUNNY",
"ALICE",
"FOR",
"BUX",
"NULS",
"BELT",
"RAMP",
"BFI",
"DEXE",
"BEL",
"TPT",
"WATCH",
"xMARK",
"bMXX",
"IOTX",
"BOR",
"bOPEN",
"DODO",
"SWINGBY",
"BRY",
"ZEE",
"SWGb",
"SWG",
"SFP",
"LINA",
"LIT",
"HGET",
"BDO",
"EGLD",
"UST",
"wSOTE",
"FRONT",
"Helmet",
"BTCST",
"BSCX",
"TEN",
"bALBT",
"ASR",
"ATM",
"OG",
"REEF",
"DITTO",
"JUV",
"PSG",
"VAI",
"wBNB",
"BLINK",
"UNFI",
"TWT",
"HARD",
"bROOBEE",
"STAX",
"NAR",
"NYA",
"CTK",
"INJ",
"SXP",
"ALPHA",
"XVS",
"SUSHI",
"COMP",
"SYRUP",
"BIFI",
"DUSK",
"BUSD",
"ETH",
"BETH",
"mAMZN",
"mGOOGL",
"mNFLX",
"mTSLA",
"LTC",
"USDC",
"DAI",
"ADA",
"BAND",
"DOT",
"EOS",
"LINK",
"USDT",
"BTCB",
"XRP",
"ATOM",
"YFII",
"XTZ",
"BCH",
"YFI",
"UNI",
"FIL",
"BAKE",
"BURGER",
"bDIGG",
"bBadger",
"TRADE",
"PNT",
"MIR",
"pBTC",
"LTO",
"pCWS",
"ZIL",
"LIEN",
"SWTH",
"DFT",
"GUM",
"DEGO",
"NRV",
"EASY",
"ODDZ",
"HOO",
"APYS",
"BONDLY",
"TKO",
"ITAM",
"ARPA",
"EPS",
"JGN",
"TLM",
"PERL",
"ALPA",
"HZN",
"SUTER",
"CGG",
"MIX",
"HAKKA",
"XED",
"τBTC",
"ALPACA",
"DFD",
"LMT",
"BTT",
"TRX",
"WIN",
"mCOIN",
"MATH",
"KUN",
"QSD",
"HYFI",
"OIN",
"DOGE",
"FINE",
"ONE",
"PMON",
"HOTCROSS",
"τDOGE",
"BTR",
"UBXT",
"WMASS",
"RFOX",
"XEND",
"CYC",
"CHR",
"KALM",
"DERI",
"WELL",
"WEX",
"WAULTx",
"pOPEN",
"EZ",
"VRT",
"TUSD",
"MTRG",
"KTN",
"QKC",
"bCFX",
"MX",
"ATA",
"MBOX",
"BORING",
"MARSH",
"AMPL",
"O3",
"HAI",
"HTB",
"WOO",
"$DG",
]
currencies = tokens.map do |token|
Currency.create!(name: token)
end
currencies.each do |currency|
random_floating_number = (rand * (10000 - 0) + 0)
Balance.create!(
user_id: user.id,
currency_id: currency.id,
amount: random_floating_number
)
end

View File

@@ -0,0 +1,35 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: balances
#
# id :bigint not null, primary key
# amount :decimal(20, 10) default(0.0), not null
# created_at :datetime not null
# updated_at :datetime not null
# currency_id :bigint not null
# user_id :bigint not null
#
# Indexes
#
# index_balances_on_currency_id (currency_id)
# index_balances_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (currency_id => currencies.id)
# fk_rails_... (user_id => users.id)
#
require "rails_helper"
RSpec.describe(Balance, type: :model) do
describe "validations" do
it { is_expected.to(validate_presence_of(:amount)) }
end
describe "associations" do
it { is_expected.to(belong_to(:user)) }
it { is_expected.to(belong_to(:currency)) }
end
end

View File

@@ -0,0 +1,18 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: currencies
#
# id :bigint not null, primary key
# name :string not null
# created_at :datetime not null
# updated_at :datetime not null
#
require "rails_helper"
RSpec.describe(Currency, type: :model) do
describe "validations" do
it { is_expected.to(validate_presence_of(:name)) }
end
end

View File

@@ -31,5 +31,6 @@ RSpec.describe(User, type: :model) do
describe "associations" do
it { is_expected.to(have_many(:documents)) }
it { is_expected.to(have_many(:balances)) }
end
end

View File

@@ -0,0 +1,6 @@
# frozen_string_literal: true
require "rails_helper"
RSpec.describe(BalancePolicy, type: :policy) do
pending "add some examples to (or delete) #{__FILE__}"
end