Merge pull request #37 from exstake/feature/unstake-request

Feature/unstake request
This commit is contained in:
João Geonizeli
2021-08-28 00:38:52 -03:00
committed by GitHub
33 changed files with 970 additions and 150 deletions

View File

@@ -28,6 +28,7 @@ gem "money-rails"
gem "enumerize" gem "enumerize"
gem "graphql" gem "graphql"
gem "pundit" gem "pundit"
gem "ransack", "~> 2.4"
group :development, :test do group :development, :test do
gem "dotenv-rails" gem "dotenv-rails"

View File

@@ -235,6 +235,10 @@ GEM
thor (~> 1.0) thor (~> 1.0)
rainbow (3.0.0) rainbow (3.0.0)
rake (13.0.6) rake (13.0.6)
ransack (2.4.2)
activerecord (>= 5.2.4)
activesupport (>= 5.2.4)
i18n
rb-fsevent (0.11.0) rb-fsevent (0.11.0)
rb-inotify (0.10.1) rb-inotify (0.10.1)
ffi (~> 1.0) ffi (~> 1.0)
@@ -373,6 +377,7 @@ DEPENDENCIES
rails (~> 6.1.4) rails (~> 6.1.4)
rails-erd rails-erd
rails-i18n (~> 6.0) rails-i18n (~> 6.0)
ransack (~> 2.4)
rspec-graphql_matchers (~> 1.3) rspec-graphql_matchers (~> 1.3)
rspec-rails rspec-rails
rubocop-rails rubocop-rails

View File

@@ -1,7 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
module Inputs module Inputs
class CreateStakeOrderAttributesInput < Types::BaseInputObject class CreateStakeOrderAttributesInput < Types::BaseInputObject
argument :currency_id, ID, required: true
argument :pool_name, String, required: true argument :pool_name, String, required: true
argument :amount, String, "Amount to be paid", required: true argument :amount, String, "Amount to be paid", required: true
end end

View File

@@ -0,0 +1,9 @@
# frozen_string_literal: true
module Inputs
class PredicateInput < Types::BaseInputObject
# https://github.com/activerecord-hackery/ransack#search-matchers
# add others if necessary
argument :eq, String, "Equal", required: false
argument :lt, Float, "Less than", required: false
end
end

View File

@@ -0,0 +1,10 @@
# frozen_string_literal: true
module Inputs
class StakeOrderFilterInput < Types::BaseInputObject
StakeOrder.ransackable_attributes.each do |attr|
argument attr, PredicateInput, required: false
end
argument :status, [Types::ProcessStatusEnum], required: false
end
end

View File

@@ -6,7 +6,7 @@ module Mutations
argument :order, Inputs::CreateStakeOrderAttributesInput, required: true argument :order, Inputs::CreateStakeOrderAttributesInput, required: true
def resolve(order:) def resolve(order:)
currency_id = decode_id(order[:currency_id]) currency_id = Currency.find_by!(name: "CAKE").id
amount = BigDecimal(order[:amount]) amount = BigDecimal(order[:amount])
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do

View File

@@ -0,0 +1,29 @@
# frozen_string_literal: true
module Mutations
class CreateStakeRemoveOrder < BaseMutation
field :order, Types::StakeOrderType, null: true
argument :order, Inputs::CreateStakeOrderAttributesInput, required: true
def resolve(order:)
currency_id = Currency.find_by!(name: "CAKE").id
amount = -BigDecimal(order[:amount])
ActiveRecord::Base.transaction do
record = StakeOrder.find_or_initialize_by(
pool_name: order[:pool_name],
user_id: current_user.id,
currency_id: currency_id,
status: :processing
)
record.amount += amount
record.save!
{ order: record }
rescue ActiveRecord::RecordInvalid => e
{ errors: Resolvers::ModelErrors.from_active_record_model(e.record) }
end
end
end
end

View File

@@ -0,0 +1,21 @@
# frozen_string_literal: true
module RansackSupport
def ransack(base, filter)
base.ransack(build_ransack_query(base, filter)).result
end
def build_ransack_query(base, filter)
filter = filter.to_h
mapped_filter = {}
filter.each do |parent_key, parent_value|
next unless base.ransackable_attributes.include?(parent_key.to_s)
parent_value.each do |children_key, children_value|
mapped_filter["#{parent_key}_#{children_key}".to_sym] = children_value
end
end
mapped_filter
end
end

View File

@@ -1,6 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
module Types module Types
class MutationType < Types::BaseObject class MutationType < Types::BaseObject
field :create_stake_remove_order, mutation: Mutations::CreateStakeRemoveOrder
field :create_stake_order, mutation: Mutations::CreateStakeOrder field :create_stake_order, mutation: Mutations::CreateStakeOrder
field :create_sell_crypto_order, mutation: Mutations::CreateSellCryptoOrder field :create_sell_crypto_order, mutation: Mutations::CreateSellCryptoOrder
field :create_buy_crypto_order, mutation: Mutations::CreateBuyCryptoOrder field :create_buy_crypto_order, mutation: Mutations::CreateBuyCryptoOrder

View File

@@ -3,6 +3,7 @@ module Types
class QueryType < Types::BaseObject class QueryType < Types::BaseObject
include GraphQL::Types::Relay::HasNodeField include GraphQL::Types::Relay::HasNodeField
include GraphQL::Types::Relay::HasNodesField include GraphQL::Types::Relay::HasNodesField
include RansackSupport
field :current_user, UserType, null: true field :current_user, UserType, null: true
def current_user def current_user
@@ -29,9 +30,16 @@ module Types
Pundit.policy_scope(current_user, BuyCryptoOrder) Pundit.policy_scope(current_user, BuyCryptoOrder)
end end
field :stake_orders, StakeOrderType.connection_type, null: false field :stake_orders, StakeOrderType.connection_type, null: false do
def stake_orders argument :filter, Inputs::StakeOrderFilterInput, required: false
Pundit.policy_scope(current_user, StakeOrder) end
def stake_orders(filter: nil)
scope = Pundit.policy_scope(current_user, StakeOrder)
scope = scope.where(status: filter.status) if filter&.status
ransack(scope, filter)
end end
end end
end end

View File

@@ -149,7 +149,6 @@ input CreateStakeOrderAttributesInput {
Amount to be paid Amount to be paid
""" """
amount: String! amount: String!
currencyId: ID!
poolName: String! poolName: String!
} }
@@ -180,6 +179,33 @@ type CreateStakeOrderPayload {
order: StakeOrder order: StakeOrder
} }
"""
Autogenerated input type of CreateStakeRemoveOrder
"""
input CreateStakeRemoveOrderInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
order: CreateStakeOrderAttributesInput!
}
"""
Autogenerated return type of CreateStakeRemoveOrder
"""
type CreateStakeRemoveOrderPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [RecordInvalid!]
order: StakeOrder
}
type Currency implements Node { type Currency implements Node {
id: ID! id: ID!
name: String! name: String!
@@ -245,6 +271,12 @@ type Mutation {
""" """
input: CreateStakeOrderInput! input: CreateStakeOrderInput!
): CreateStakeOrderPayload ): CreateStakeOrderPayload
createStakeRemoveOrder(
"""
Parameters for CreateStakeRemoveOrder
"""
input: CreateStakeRemoveOrderInput!
): CreateStakeRemoveOrderPayload
} }
""" """
@@ -282,6 +314,18 @@ type PageInfo {
startCursor: String startCursor: String
} }
input PredicateInput {
"""
Equal
"""
eq: String
"""
Less than
"""
lt: Float
}
enum ProcessStatus { enum ProcessStatus {
CANCELED CANCELED
COMPLETED COMPLETED
@@ -404,6 +448,7 @@ type Query {
Returns the elements in the list that come before the specified cursor. Returns the elements in the list that come before the specified cursor.
""" """
before: String before: String
filter: StakeOrderFilterInput
""" """
Returns the first _n_ elements from the list. Returns the first _n_ elements from the list.
@@ -503,6 +548,12 @@ type StakeOrderEdge {
node: StakeOrder! node: StakeOrder!
} }
input StakeOrderFilterInput {
amount: PredicateInput
poolName: PredicateInput
status: [ProcessStatus!]
}
type User { type User {
email: String! email: String!
firstName: String! firstName: String!

View File

@@ -1,12 +1,15 @@
import type { FC } from "react"; import type { FC } from "react";
import React from "react"; import React from "react";
import useSWR from "swr"; import useSWR from "swr";
import cx from "classnames";
import { useCurrentUser } from "../../contexts/UserProvider"; import { useCurrentUser } from "../../contexts/UserProvider";
import type { Vault, YieldwatchResponse } from "../../types/yieldwatch"; import type {
Vault as VaultType,
YieldwatchResponse,
} from "../../types/yieldwatch";
import { Vault } from "./Vault";
const exampleVault: Partial<Vault> = { const exampleVault: Partial<VaultType> = {
chainId: 1, chainId: 1,
name: "Cake-Cake Staking", name: "Cake-Cake Staking",
depositedTokens: 0, depositedTokens: 0,
@@ -28,57 +31,7 @@ export const Dashbaord: FC = () => {
<div className="grid place-items-center w-full h-5 mt-32"> <div className="grid place-items-center w-full h-5 mt-32">
<div className="max-w-3xl grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="max-w-3xl grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{vaults?.map((vault) => ( {vaults?.map((vault) => (
<div <Vault key={vault.name} isLoading={isLoading} vault={vault} />
key={vault.chainId}
className="shadow-lg px-4 py-6 w-full bg-white dark:bg-gray-800 rounded-lg"
>
<p className="text-sm w-max text-gray-700 dark:text-white font-semibold border-b border-gray-200">
{vault.name}
</p>
<div className="flex items-end space-x-2 my-6">
<p
className={cx(
"text-5xl text-black dark:text-white font-bold",
isLoading
? "w-36 h-10 inline-block animate-pulse bg-gray-300 rounded"
: ""
)}
>
{!isLoading &&
(
(vault.depositedTokens ?? 0) + (vault.totalRewards ?? 0)
).toFixed(4)}
</p>
</div>
<div className="dark:text-white">
<div className="flex items-center pb-2 mb-2 text-sm space-x-12 md:space-x-24 justify-between border-b border-gray-200">
<p>Depositado</p>
<div
className={cx(
"flex items-end text-xs",
isLoading
? "w-10 h-4 inline-block animate-pulse bg-gray-300 rounded"
: ""
)}
>
{!isLoading && vault.depositedTokens?.toFixed(4)}
</div>
</div>
<div className="flex items-center text-sm space-x-12 md:space-x-24 justify-between">
<p>Ganho</p>
<div
className={cx(
"flex items-end text-xs",
isLoading
? "w-10 h-4 inline-block animate-pulse bg-gray-300 rounded"
: ""
)}
>
{!isLoading && vault.totalRewards?.toFixed(4)}
</div>
</div>
</div>
</div>
))} ))}
</div> </div>
</div> </div>

View File

@@ -0,0 +1,103 @@
import BigNumber from "bignumber.js";
import type { ChangeEvent, FC } from "react";
import React, { useState } from "react";
import cx from "classnames";
import { useRelayEnvironment } from "react-relay";
import { Modal } from "../../../../components";
import { commitCreateStakeRemoveOrderMutation } from "./commitCreateStakeRemoveOrder";
const inputBaseStyles =
"rounded-lg border-transparent flex-1 appearance-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-transparent mb-3";
type RemoveStakeModal = {
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
isOpen: boolean;
stakedCake: string;
poolName?: string;
};
export const RemoveStakeModal: FC<RemoveStakeModal> = ({
setIsOpen,
isOpen,
stakedCake,
poolName = "",
}) => {
const enviroment = useRelayEnvironment();
const [amountInput, setAmountInput] = useState<string>("0");
const avaliableCake = BigNumber.sum(stakedCake);
const handleClose = () => {
setIsOpen(false);
};
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
commitCreateStakeRemoveOrderMutation(enviroment, {
poolName,
amount: amountInput,
});
};
const handleInvestInput = ({
currentTarget: { value },
}: ChangeEvent<HTMLInputElement>) => {
const newInvestAmount = new BigNumber(value);
if (
newInvestAmount.isLessThanOrEqualTo(avaliableCake) &&
newInvestAmount.isGreaterThanOrEqualTo(0)
) {
setAmountInput(value);
}
};
const handleMaxButton = () => {
setAmountInput(stakedCake);
};
const amountToUnstake = new BigNumber(amountInput);
const stakeAvaliable =
amountToUnstake.isGreaterThan(0) &&
amountToUnstake.isLessThanOrEqualTo(avaliableCake);
return (
<Modal
isOpen={isOpen}
setIsOpen={handleClose}
title={`Remover investido em ${poolName}`}
>
<span className="mb-2">CAKE disponível: {stakedCake}</span>
<form onSubmit={onSubmit} className="bg-white py-2">
<div className="flex flex-row">
<input
className={cx(inputBaseStyles)}
type="number"
value={amountInput}
onChange={handleInvestInput}
/>
<button
type="button"
disabled={amountInput === stakedCake}
className="flex items-center mb-3 ml-3 font-bold rounded-full text-red-500"
onClick={handleMaxButton}
>
Max
</button>
</div>
{avaliableCake.isEqualTo(0) && (
<span className="text-red-500 mb-1">Você não possuí saldo.</span>
)}
<button
className="cursor-pointer py-2 px-4 disabled:opacity-50 disabled:cursor-default bg-red-600 hover:bg-red-700 focus:ring-red-500 focus:ring-offset-red-200 text-white w-full transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-lg"
disabled={!stakeAvaliable}
type="submit"
>
Remover Stake
</button>
</form>
</Modal>
);
};

View File

@@ -0,0 +1,136 @@
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from "relay-runtime";
export type commitCreateStakeRemoveOrderMutationVariables = {
poolName: string;
amount: string;
};
export type commitCreateStakeRemoveOrderMutationResponse = {
readonly createStakeRemoveOrder: {
readonly order: {
readonly id: string;
} | null;
} | null;
};
export type commitCreateStakeRemoveOrderMutation = {
readonly response: commitCreateStakeRemoveOrderMutationResponse;
readonly variables: commitCreateStakeRemoveOrderMutationVariables;
};
/*
mutation commitCreateStakeRemoveOrderMutation(
$poolName: String!
$amount: String!
) {
createStakeRemoveOrder(input: {order: {poolName: $poolName, amount: $amount}}) {
order {
id
}
}
}
*/
const node: ConcreteRequest = (function(){
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "amount"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "poolName"
},
v2 = [
{
"alias": null,
"args": [
{
"fields": [
{
"fields": [
{
"kind": "Variable",
"name": "amount",
"variableName": "amount"
},
{
"kind": "Variable",
"name": "poolName",
"variableName": "poolName"
}
],
"kind": "ObjectValue",
"name": "order"
}
],
"kind": "ObjectValue",
"name": "input"
}
],
"concreteType": "CreateStakeRemoveOrderPayload",
"kind": "LinkedField",
"name": "createStakeRemoveOrder",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "StakeOrder",
"kind": "LinkedField",
"name": "order",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "commitCreateStakeRemoveOrderMutation",
"selections": (v2/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "commitCreateStakeRemoveOrderMutation",
"selections": (v2/*: any*/)
},
"params": {
"cacheID": "a3a646d6f52bf3ddc29e33b8fce4661b",
"id": null,
"metadata": {},
"name": "commitCreateStakeRemoveOrderMutation",
"operationKind": "mutation",
"text": "mutation commitCreateStakeRemoveOrderMutation(\n $poolName: String!\n $amount: String!\n) {\n createStakeRemoveOrder(input: {order: {poolName: $poolName, amount: $amount}}) {\n order {\n id\n }\n }\n}\n"
}
};
})();
(node as any).hash = '561be0497e5317997815bea692b73da9';
export default node;

View File

@@ -0,0 +1,32 @@
import { graphql } from "babel-plugin-relay/macro";
import type { Environment } from "react-relay";
import { commitMutation } from "react-relay";
import type { commitCreateStakeRemoveOrderMutationVariables } from "./__generated__/commitCreateStakeRemoveOrderMutation.graphql";
export const commitCreateStakeRemoveOrderMutation = (
environment: Environment,
variables: commitCreateStakeRemoveOrderMutationVariables
) => {
return commitMutation(environment, {
mutation: graphql`
mutation commitCreateStakeRemoveOrderMutation(
$poolName: String!
$amount: String!
) {
createStakeRemoveOrder(
input: { order: { poolName: $poolName, amount: $amount } }
) {
order {
id
}
}
}
`,
variables,
onCompleted: (_response) => {
window.location.href = "/orders/stake";
},
onError: (_error) => {},
});
};

View File

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

View File

@@ -0,0 +1,139 @@
import type { FC } from "react";
import React, { useState } from "react";
import cx from "classnames";
import { XCircleIcon } from "@heroicons/react/outline";
import { useLazyLoadQuery } from "react-relay";
import { graphql } from "babel-plugin-relay/macro";
import BigNumber from "bignumber.js";
import type { Vault as VaultType } from "../../../types/yieldwatch";
import { RemoveStakeModal } from "./RemoveStakeModal";
import type { VaultQuery } from "./__generated__/VaultQuery.graphql";
type VaultProps = {
vault: Partial<VaultType>;
isLoading: boolean;
};
export const Vault: FC<VaultProps> = ({ vault, isLoading }) => {
const { stakeOrders } = useLazyLoadQuery<VaultQuery>(
graphql`
query VaultQuery(
$status: ProcessStatus!
$poolName: String!
$amount: Float!
) {
stakeOrders(
filter: {
status: [$status]
poolName: { eq: $poolName }
amount: { lt: $amount }
}
) {
edges {
node {
amount
}
}
}
}
`,
{
status: "PROCESSING",
poolName: vault.name ?? "",
amount: 0,
}
);
const [removeStakeModalIsOpen, setRemoveStakeModalIsOpen] =
useState<boolean>(false);
const handleRemoveStakeModal = () => {
setRemoveStakeModalIsOpen(true);
};
const alreadyOnUnstakeOrder = stakeOrders.edges.reduce((acc, current) => {
return BigNumber.sum(acc, current.node.amount);
}, new BigNumber(0));
const totalDepositedAndRewarded =
(vault.depositedTokens ?? 0) + (vault.totalRewards ?? 0);
let totalStaked = BigNumber.sum(
alreadyOnUnstakeOrder,
totalDepositedAndRewarded
);
totalStaked = totalStaked.isLessThan(0.0001) ? new BigNumber(0) : totalStaked;
const totalStakedFixed = totalStaked.toFixed(4);
const totalDeposited = (
totalStaked.isEqualTo(0) ? 0 : vault.depositedTokens
)?.toFixed(4);
const totalRewarded = (
totalStaked.isEqualTo(0) ? 0 : vault.totalRewards
)?.toFixed(4);
return (
<>
<RemoveStakeModal
isOpen={removeStakeModalIsOpen}
setIsOpen={setRemoveStakeModalIsOpen}
stakedCake={totalStakedFixed}
poolName={vault.name}
/>
<div className="shadow-lg px-4 py-6 w-full bg-white dark:bg-gray-800 rounded-lg">
<div className="flex justify-between">
<p className="text-sm w-max text-gray-700 dark:text-white font-semibold border-b border-gray-200">
{vault.name}
</p>
<button onClick={handleRemoveStakeModal} aria-label="Remover Stake">
<XCircleIcon className="h-5 w-5 text-red-500" />
</button>
</div>
<div className="flex items-end space-x-2 my-6">
<p
className={cx(
"text-5xl text-black dark:text-white font-bold",
isLoading
? "w-36 h-10 inline-block animate-pulse bg-gray-300 rounded"
: ""
)}
>
{!isLoading && totalStakedFixed}
</p>
</div>
<div className="dark:text-white">
<div className="flex items-center pb-2 mb-2 text-sm space-x-12 md:space-x-24 justify-between border-b border-gray-200">
<p>Depositado</p>
<div
className={cx(
"flex items-end text-xs",
isLoading
? "w-10 h-4 inline-block animate-pulse bg-gray-300 rounded"
: ""
)}
>
{!isLoading && totalDeposited}
</div>
</div>
<div className="flex items-center text-sm space-x-12 md:space-x-24 justify-between">
<p>Ganho</p>
<div
className={cx(
"flex items-end text-xs",
isLoading
? "w-10 h-4 inline-block animate-pulse bg-gray-300 rounded"
: ""
)}
>
{!isLoading && totalRewarded}
</div>
</div>
</div>
</div>
</>
);
};

View File

@@ -0,0 +1,222 @@
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from "relay-runtime";
export type ProcessStatus = "CANCELED" | "COMPLETED" | "PROCESSING" | "%future added value";
export type VaultQueryVariables = {
status: ProcessStatus;
poolName: string;
amount: number;
};
export type VaultQueryResponse = {
readonly stakeOrders: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly amount: string;
};
}>;
};
};
export type VaultQuery = {
readonly response: VaultQueryResponse;
readonly variables: VaultQueryVariables;
};
/*
query VaultQuery(
$status: ProcessStatus!
$poolName: String!
$amount: Float!
) {
stakeOrders(filter: {status: [$status], poolName: {eq: $poolName}, amount: {lt: $amount}}) {
edges {
node {
amount
id
}
}
}
}
*/
const node: ConcreteRequest = (function(){
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "amount"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "poolName"
},
v2 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "status"
},
v3 = [
{
"fields": [
{
"fields": [
{
"kind": "Variable",
"name": "lt",
"variableName": "amount"
}
],
"kind": "ObjectValue",
"name": "amount"
},
{
"fields": [
{
"kind": "Variable",
"name": "eq",
"variableName": "poolName"
}
],
"kind": "ObjectValue",
"name": "poolName"
},
{
"items": [
{
"kind": "Variable",
"name": "status.0",
"variableName": "status"
}
],
"kind": "ListValue",
"name": "status"
}
],
"kind": "ObjectValue",
"name": "filter"
}
],
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "amount",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/),
(v2/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "VaultQuery",
"selections": [
{
"alias": null,
"args": (v3/*: any*/),
"concreteType": "StakeOrderConnection",
"kind": "LinkedField",
"name": "stakeOrders",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "StakeOrderEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "StakeOrder",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v4/*: any*/)
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v2/*: any*/),
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "VaultQuery",
"selections": [
{
"alias": null,
"args": (v3/*: any*/),
"concreteType": "StakeOrderConnection",
"kind": "LinkedField",
"name": "stakeOrders",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "StakeOrderEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "StakeOrder",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v4/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "6565697f7f43e955abee8a8a1eeb8e9b",
"id": null,
"metadata": {},
"name": "VaultQuery",
"operationKind": "query",
"text": "query VaultQuery(\n $status: ProcessStatus!\n $poolName: String!\n $amount: Float!\n) {\n stakeOrders(filter: {status: [$status], poolName: {eq: $poolName}, amount: {lt: $amount}}) {\n edges {\n node {\n amount\n id\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = 'e5dced4589579717b408bc2f555b98ab';
export default node;

View File

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

View File

@@ -11,10 +11,9 @@ import { StakeOrderModal } from "./StakeOrderModal";
type PoolProps = { type PoolProps = {
pool: PoolConfig; pool: PoolConfig;
balance: string; balance: string;
currencyId: string;
}; };
export const Pool: FC<PoolProps> = ({ pool, balance, currencyId }) => { export const Pool: FC<PoolProps> = ({ pool, balance }) => {
const { const {
provider, provider,
pancake: { router }, pancake: { router },
@@ -36,7 +35,7 @@ export const Pool: FC<PoolProps> = ({ pool, balance, currencyId }) => {
const totalStaked = await getTotalStaked(provider, pool); const totalStaked = await getTotalStaked(provider, pool);
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log( console.info(
`Total Staked for ${pool.stakingToken.symbol} - ${ `Total Staked for ${pool.stakingToken.symbol} - ${
pool.earningToken.symbol pool.earningToken.symbol
}: ${JSON.stringify(totalStaked)}` }: ${JSON.stringify(totalStaked)}`
@@ -94,7 +93,6 @@ export const Pool: FC<PoolProps> = ({ pool, balance, currencyId }) => {
<StakeOrderModal <StakeOrderModal
poolName={pool.earningToken.symbol} poolName={pool.earningToken.symbol}
balance={balance} balance={balance}
currencyId={currencyId}
/> />
</div> </div>
</div> </div>

View File

@@ -14,7 +14,6 @@ export const PoolListing = () => {
edges { edges {
node { node {
currency { currency {
id
name name
} }
amount amount
@@ -31,7 +30,6 @@ export const PoolListing = () => {
)?.node; )?.node;
const balance = cakeBalance?.amount ?? "0"; const balance = cakeBalance?.amount ?? "0";
const currencyId = cakeBalance?.currency.id ?? "????";
return ( return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 place-items-center w-full gap-8 py-4 -mt-16 overflow-x-hidden"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 place-items-center w-full gap-8 py-4 -mt-16 overflow-x-hidden">
@@ -39,12 +37,7 @@ export const PoolListing = () => {
.filter((pool) => !pool.isFinished) .filter((pool) => !pool.isFinished)
.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)) .sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))
.map((pool) => ( .map((pool) => (
<Pool <Pool key={pool.sousId} pool={pool} balance={balance} />
key={pool.sousId}
pool={pool}
balance={balance}
currencyId={currencyId}
/>
))} ))}
</div> </div>
); );

View File

@@ -10,17 +10,12 @@ import { commitCreateStakeOrderMutation } from "./createStakeOrder";
type Props = { type Props = {
poolName: string; poolName: string;
balance: string; balance: string;
currencyId: string;
}; };
const inputBaseStyles = const inputBaseStyles =
"rounded-lg border-transparent flex-1 appearance-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-transparent mb-3"; "rounded-lg border-transparent flex-1 appearance-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-transparent mb-3";
export const StakeOrderModal: FC<Props> = ({ export const StakeOrderModal: FC<Props> = ({ poolName, balance }) => {
poolName,
balance,
currencyId,
}) => {
const environment = useRelayEnvironment(); const environment = useRelayEnvironment();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [investAmountInput, setInvestAmountInput] = useState("0"); const [investAmountInput, setInvestAmountInput] = useState("0");
@@ -34,7 +29,6 @@ export const StakeOrderModal: FC<Props> = ({
const onSubmit = () => { const onSubmit = () => {
commitCreateStakeOrderMutation(environment, { commitCreateStakeOrderMutation(environment, {
currencyId,
amount: investAmountInput, amount: investAmountInput,
poolName, poolName,
}); });

View File

@@ -4,7 +4,6 @@
import { ConcreteRequest } from "relay-runtime"; import { ConcreteRequest } from "relay-runtime";
export type createStakeOrderMutationVariables = { export type createStakeOrderMutationVariables = {
currencyId: string;
poolName: string; poolName: string;
amount: string; amount: string;
}; };
@@ -24,11 +23,10 @@ export type createStakeOrderMutation = {
/* /*
mutation createStakeOrderMutation( mutation createStakeOrderMutation(
$currencyId: ID!
$poolName: String! $poolName: String!
$amount: String! $amount: String!
) { ) {
createStakeOrder(input: {order: {currencyId: $currencyId, poolName: $poolName, amount: $amount}}) { createStakeOrder(input: {order: {poolName: $poolName, amount: $amount}}) {
order { order {
id id
} }
@@ -43,16 +41,11 @@ var v0 = {
"name": "amount" "name": "amount"
}, },
v1 = { v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "currencyId"
},
v2 = {
"defaultValue": null, "defaultValue": null,
"kind": "LocalArgument", "kind": "LocalArgument",
"name": "poolName" "name": "poolName"
}, },
v3 = [ v2 = [
{ {
"alias": null, "alias": null,
"args": [ "args": [
@@ -65,11 +58,6 @@ v3 = [
"name": "amount", "name": "amount",
"variableName": "amount" "variableName": "amount"
}, },
{
"kind": "Variable",
"name": "currencyId",
"variableName": "currencyId"
},
{ {
"kind": "Variable", "kind": "Variable",
"name": "poolName", "name": "poolName",
@@ -115,13 +103,12 @@ return {
"fragment": { "fragment": {
"argumentDefinitions": [ "argumentDefinitions": [
(v0/*: any*/), (v0/*: any*/),
(v1/*: any*/), (v1/*: any*/)
(v2/*: any*/)
], ],
"kind": "Fragment", "kind": "Fragment",
"metadata": null, "metadata": null,
"name": "createStakeOrderMutation", "name": "createStakeOrderMutation",
"selections": (v3/*: any*/), "selections": (v2/*: any*/),
"type": "Mutation", "type": "Mutation",
"abstractKey": null "abstractKey": null
}, },
@@ -129,22 +116,21 @@ return {
"operation": { "operation": {
"argumentDefinitions": [ "argumentDefinitions": [
(v1/*: any*/), (v1/*: any*/),
(v2/*: any*/),
(v0/*: any*/) (v0/*: any*/)
], ],
"kind": "Operation", "kind": "Operation",
"name": "createStakeOrderMutation", "name": "createStakeOrderMutation",
"selections": (v3/*: any*/) "selections": (v2/*: any*/)
}, },
"params": { "params": {
"cacheID": "bfe4935c593947810fbb8d7a52421483", "cacheID": "e845ef953b2de9dd797930c0838f30f8",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "createStakeOrderMutation", "name": "createStakeOrderMutation",
"operationKind": "mutation", "operationKind": "mutation",
"text": "mutation createStakeOrderMutation(\n $currencyId: ID!\n $poolName: String!\n $amount: String!\n) {\n createStakeOrder(input: {order: {currencyId: $currencyId, poolName: $poolName, amount: $amount}}) {\n order {\n id\n }\n }\n}\n" "text": "mutation createStakeOrderMutation(\n $poolName: String!\n $amount: String!\n) {\n createStakeOrder(input: {order: {poolName: $poolName, amount: $amount}}) {\n order {\n id\n }\n }\n}\n"
} }
}; };
})(); })();
(node as any).hash = '036f321e28fcb4bd3e274498cd3f116a'; (node as any).hash = '36f248efe00b47bc1b27f597c5ab45c3';
export default node; export default node;

View File

@@ -10,19 +10,9 @@ export const commitCreateStakeOrderMutation = (
) => { ) => {
return commitMutation(environment, { return commitMutation(environment, {
mutation: graphql` mutation: graphql`
mutation createStakeOrderMutation( mutation createStakeOrderMutation($poolName: String!, $amount: String!) {
$currencyId: ID!
$poolName: String!
$amount: String!
) {
createStakeOrder( createStakeOrder(
input: { input: { order: { poolName: $poolName, amount: $amount } }
order: {
currencyId: $currencyId
poolName: $poolName
amount: $amount
}
}
) { ) {
order { order {
id id
@@ -30,7 +20,7 @@ export const commitCreateStakeOrderMutation = (
} }
} }
`, `,
variables: { ...variables }, variables,
onCompleted: (_response) => { onCompleted: (_response) => {
window.location.reload(); window.location.reload();
}, },

View File

@@ -9,7 +9,6 @@ export type PoolListingQueryResponse = {
readonly edges: ReadonlyArray<{ readonly edges: ReadonlyArray<{
readonly node: { readonly node: {
readonly currency: { readonly currency: {
readonly id: string;
readonly name: string; readonly name: string;
}; };
readonly amount: string; readonly amount: string;
@@ -30,8 +29,8 @@ query PoolListingQuery {
edges { edges {
node { node {
currency { currency {
id
name name
id
} }
amount amount
id id
@@ -46,33 +45,21 @@ var v0 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "id", "name": "name",
"storageKey": null "storageKey": null
}, },
v1 = { v1 = {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "Currency", "kind": "ScalarField",
"kind": "LinkedField", "name": "amount",
"name": "currency",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
}
],
"storageKey": null "storageKey": null
}, },
v2 = { v2 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "amount", "name": "id",
"storageKey": null "storageKey": null
}; };
return { return {
@@ -106,8 +93,19 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v1/*: any*/), {
(v2/*: any*/) "alias": null,
"args": null,
"concreteType": "Currency",
"kind": "LinkedField",
"name": "currency",
"plural": false,
"selections": [
(v0/*: any*/)
],
"storageKey": null
},
(v1/*: any*/)
], ],
"storageKey": null "storageKey": null
} }
@@ -151,9 +149,21 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
{
"alias": null,
"args": null,
"concreteType": "Currency",
"kind": "LinkedField",
"name": "currency",
"plural": false,
"selections": [
(v0/*: any*/),
(v2/*: any*/)
],
"storageKey": null
},
(v1/*: any*/), (v1/*: any*/),
(v2/*: any*/), (v2/*: any*/)
(v0/*: any*/)
], ],
"storageKey": null "storageKey": null
} }
@@ -166,14 +176,14 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "06c9467183eb0e89329ec630a8cc4880", "cacheID": "6abf5e963429e49993af50df156f8e1c",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "PoolListingQuery", "name": "PoolListingQuery",
"operationKind": "query", "operationKind": "query",
"text": "query PoolListingQuery {\n balances {\n edges {\n node {\n currency {\n id\n name\n }\n amount\n id\n }\n }\n }\n}\n" "text": "query PoolListingQuery {\n balances {\n edges {\n node {\n currency {\n name\n id\n }\n amount\n id\n }\n }\n }\n}\n"
} }
}; };
})(); })();
(node as any).hash = '570efc1d3b5dac09303b8692d6830bb2'; (node as any).hash = '4fefb238e24b79198799686599255e6c';
export default node; export default node;

View File

@@ -33,6 +33,10 @@ class StakeOrder < ApplicationRecord
validates :pool_name, presence: true validates :pool_name, presence: true
validates :amount, presence: true validates :amount, presence: true
def self.ransackable_attributes(auth_object = nil)
super & ["pool_name", "amount"]
end
private private
def notification_message def notification_message

View File

@@ -7,8 +7,8 @@
"lint": "eslint --ext .jsx,.js,.tsx,.ts app/javascript/", "lint": "eslint --ext .jsx,.js,.tsx,.ts app/javascript/",
"lint:fix": "eslint --fix --ext .jsx,.js,.tsx,.ts app/javascript/", "lint:fix": "eslint --fix --ext .jsx,.js,.tsx,.ts app/javascript/",
"tsc": "tsc --noEmit", "tsc": "tsc --noEmit",
"relay": "relay-compiler --schema app/javascript/__generated__/schema.graphql --src app/javascript/src --extensions tsx --language typescript", "relay": "relay-compiler",
"relay:watch": "relay-compiler --schema app/javascript/__generated__/schema.graphql --src app/javascript/src --extensions tsx --language typescript --watch" "relay:watch": "yarn relay --watch"
}, },
"husky": { "husky": {
"hooks": { "hooks": {
@@ -19,6 +19,7 @@
"@babel/preset-react": "^7.14.5", "@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.14.5", "@babel/preset-typescript": "^7.14.5",
"@headlessui/react": "^1.4.0", "@headlessui/react": "^1.4.0",
"@heroicons/react": "^1.0.4",
"@rails/actioncable": "^6.0.0", "@rails/actioncable": "^6.0.0",
"@rails/activestorage": "^6.0.0", "@rails/activestorage": "^6.0.0",
"@rails/ujs": "^6.0.0", "@rails/ujs": "^6.0.0",
@@ -61,6 +62,7 @@
"prettier": "^2.3.2", "prettier": "^2.3.2",
"relay-compiler": "^11.0.2", "relay-compiler": "^11.0.2",
"relay-compiler-language-typescript": "^14.0.0", "relay-compiler-language-typescript": "^14.0.0",
"relay-config": "^11.0.2",
"webpack-dev-server": "^3.11.2" "webpack-dev-server": "^3.11.2"
} }
} }

6
relay.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
src: "app/javascript/src",
schema: "app/javascript/__generated__/schema.graphql",
language: "typescript",
exclude: ["**/__generated__/**"],
};

View File

@@ -6,13 +6,11 @@ RSpec.describe(Mutations::CreateStakeOrder, type: :mutation) do
let(:query_string) do let(:query_string) do
<<~GQL <<~GQL
mutation( mutation(
$currencyId: ID!,
$amount: String!, $amount: String!,
$poolName: String!, $poolName: String!,
) { ) {
createStakeOrder(input: { createStakeOrder(input: {
order: { order: {
currencyId: $currencyId,
amount: $amount, amount: $amount,
poolName: $poolName, poolName: $poolName,
} }
@@ -43,10 +41,7 @@ RSpec.describe(Mutations::CreateStakeOrder, type: :mutation) do
] ]
) )
currency_global_id = GraphQL::Schema::UniqueWithinType.encode("Currency", currency.id)
variables = { variables = {
"currencyId": currency_global_id,
"amount": "0.80", "amount": "0.80",
"poolName": "CAKE/BNB", "poolName": "CAKE/BNB",
"status": "PROCESSING", "status": "PROCESSING",
@@ -87,10 +82,7 @@ RSpec.describe(Mutations::CreateStakeOrder, type: :mutation) do
] ]
) )
currency_global_id = GraphQL::Schema::UniqueWithinType.encode("Currency", currency.id)
variables = { variables = {
"currencyId": currency_global_id,
"amount": "0.80", "amount": "0.80",
"poolName": "CAKE/BNB", "poolName": "CAKE/BNB",
} }

View File

@@ -0,0 +1,112 @@
# frozen_string_literal: true
require "rails_helper"
RSpec.describe(Mutations::CreateStakeRemoveOrder, type: :mutation) do
let(:query_string) do
<<~GQL
mutation(
$amount: String!,
$poolName: String!,
) {
createStakeRemoveOrder(input: {
order: {
amount: $amount,
poolName: $poolName,
}
}) {
errors {
fullMessages
fieldName
messages
path
}
order {
poolName
status
amount
}
}
}
GQL
end
context "when the user has enough balance" do
it "withdraws from his account and creates a buy order" do
currency = create(:currency)
user = create(
:user,
balances: [
build(:balance, currency: currency, amount: 0),
]
)
variables = {
"amount": "200.80",
"poolName": "CAKE/BNB",
"status": "PROCESSING",
}
context = { current_user: user }
result = XStakeSchema.execute(
query_string,
variables: variables,
context: context
).to_h.with_indifferent_access
expect(result).to(eq({
"data" => {
"createStakeRemoveOrder" => {
"errors" => nil,
"order" => {
"status" => "PROCESSING",
"amount" => "-200.8",
"poolName" => "CAKE/BNB",
},
},
},
}))
end
end
context "when it repeats the mutation with a request in `processing`" do
it "update amount from the order" do
currency = create(:currency)
user = create(
:user,
balances: [
build(:balance, currency: currency, amount: 0),
]
)
create(:stake_order, amount: -200.8, user: user, pool_name: "CAKE/BNB", currency: currency)
variables = {
"amount": "200.80",
"poolName": "CAKE/BNB",
}
context = { current_user: user }
result = XStakeSchema.execute(
query_string,
variables: variables,
context: context
).to_h.with_indifferent_access
expect(result).to(eq({
"data" => {
"createStakeRemoveOrder" => {
"errors" => nil,
"order" => {
"status" => "PROCESSING",
"amount" => "-401.6",
"poolName" => "CAKE/BNB",
},
},
},
}))
end
end
end

14
yarn.lock generated
View File

@@ -1472,6 +1472,11 @@
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.4.0.tgz#c6d424d8ab10ac925e4423d7f3cbab02c30d736a" resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.4.0.tgz#c6d424d8ab10ac925e4423d7f3cbab02c30d736a"
integrity sha512-C+FmBVF6YGvqcEI5fa2dfVbEaXr2RGR6Kw1E5HXIISIZEfsrH/yuCgsjWw5nlRF9vbCxmQ/EKs64GAdKeb8gCw== integrity sha512-C+FmBVF6YGvqcEI5fa2dfVbEaXr2RGR6Kw1E5HXIISIZEfsrH/yuCgsjWw5nlRF9vbCxmQ/EKs64GAdKeb8gCw==
"@heroicons/react@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.4.tgz#11847eb2ea5510419d7ada9ff150a33af0ad0863"
integrity sha512-3kOrTmo8+Z8o6AL0rzN82MOf8J5CuxhRLFhpI8mrn+3OqekA6d5eb1GYO3EYYo1Vn6mYQSMNTzCWbEwUInb0cQ==
"@humanwhocodes/config-array@^0.5.0": "@humanwhocodes/config-array@^0.5.0":
version "0.5.0" version "0.5.0"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9"
@@ -3057,7 +3062,7 @@ core-util-is@~1.0.0:
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
cosmiconfig@^5.0.0: cosmiconfig@^5.0.0, cosmiconfig@^5.0.5:
version "5.2.1" version "5.2.1"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a"
integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==
@@ -7844,6 +7849,13 @@ relay-compiler@^11.0.2:
signedsource "^1.0.0" signedsource "^1.0.0"
yargs "^15.3.1" yargs "^15.3.1"
relay-config@^11.0.2:
version "11.0.2"
resolved "https://registry.yarnpkg.com/relay-config/-/relay-config-11.0.2.tgz#d1e5bbac795dfe0a414ed61c94faabdef7db99c5"
integrity sha512-j/bl04lGwZ+xSM/21KN87lPXY6t7YWkStfST63dQhJN35F6gQKZevmxVVPlEJ7Qs41AyrY1kilGBIfbEZPPdSA==
dependencies:
cosmiconfig "^5.0.5"
relay-runtime@11.0.2, relay-runtime@^11.0.2: relay-runtime@11.0.2, relay-runtime@^11.0.2:
version "11.0.2" version "11.0.2"
resolved "https://registry.yarnpkg.com/relay-runtime/-/relay-runtime-11.0.2.tgz#c3650477d45665b9628b852b35f203e361ad55e8" resolved "https://registry.yarnpkg.com/relay-runtime/-/relay-runtime-11.0.2.tgz#c3650477d45665b9628b852b35f203e361ad55e8"