Skip to content

Commit

Permalink
[app] implemented money service, fetching balance and sources
Browse files Browse the repository at this point in the history
  • Loading branch information
arranfletcher committed Feb 10, 2020
1 parent 9d6a3b8 commit 42705d5
Show file tree
Hide file tree
Showing 16 changed files with 247 additions and 35 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ lerna-debug.log*
# Config
/config/*
!/config/default.json

# Docs
/documentation
5 changes: 5 additions & 0 deletions config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@
"wallet": {
"app": "",
"endpoint": "",
"api": "",
"client": 0,
"secret": "",
"redirect_uri": "",
"scope": ""
},
"fx": {
"url": "",
"client_id": ""
}
}
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"crypto-js": "^3.1.9-1",
"decimal.js": "^10.2.0",
"discord.js": "^11.5.1",
"money": "^0.2.0",
"nestjs-redis": "^1.2.5",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.0",
Expand Down
35 changes: 20 additions & 15 deletions src/app.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import config from 'config';
import { ProfileService } from './shared/services/profile/profile.service';
import { Profile } from './shared/core/profile';
import { Observable, of } from 'rxjs';
import { map, tap, catchError } from 'rxjs/operators'
import { map, tap, catchError, mergeMap } from 'rxjs/operators'

@Controller()
export class AppController {
Expand Down Expand Up @@ -35,6 +35,9 @@ export class AppController {
return of('Error Linking account, please try again')
}

// Create profile
const user = new Profile({ id })

// Exchange code for token
return this.http.post(`${config.get('wallet.endpoint')}/oauth/token`, {
grant_type: 'authorization_code',
Expand All @@ -43,28 +46,30 @@ export class AppController {
redirect_uri: config.get('wallet.redirect_uri'),
code
}).pipe(
// Pull out token and save
map(d => d.data.access_token),
tap(token => console.log('Token:', token)),
map(token => {
// TODO: Get data from server

// Create profile and save
const user = new Profile({
id,
token,
balance: {
available: 0,
pending: 0
},
currency: 'SGD'
})
tap(token => Object.assign(user, { token })),
// Get data from server
mergeMap(token => this.http.get(
`${config.get('wallet.api')}/me`, {
headers: {
'Authorization': `Bearer ${token}`
}
}
)),
map(a => a.data),
map(account => {
// Set user currency
user.currency = account.defaultCurrency;
console.log('Account', user);
console.log('JSON:', JSON.stringify(user));
this._profile.save(user);

// Return success page
// Return success
return 'Account successfully linked!'
}),
// Return error
catchError(e => {
console.warn(e);
return 'Error occured';
Expand Down
12 changes: 12 additions & 0 deletions src/discord/core/balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export interface BalanceAmount {
amount: number;
currency: string;
}

export interface Balance {
object: string;
available: BalanceAmount[];
connect_reserved?: BalanceAmount[];
livemode: boolean;
pending: BalanceAmount[];
}
18 changes: 18 additions & 0 deletions src/discord/core/currency-decimal-places.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export enum CurrencyDecimalPlaces {
DKK = 2,
EUR = 2,
NOK = 2,
PLN = 2,
SEK = 2,
CHF = 2,
AUD = 2,
CAD = 2,
HKD = 2,
INR = 2,
MXN = 2,
NZD = 2,
SGD = 2,
GBP = 2,
USD = 2,
JPY = 0,
}
18 changes: 18 additions & 0 deletions src/discord/core/currency-minimum-amount.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export enum CurrencyMinimumAmount {
DKK = 2.50,
EUR = 1,
NOK = 3,
PLN = 1,
SEK = 3,
CHF = 1,
AUD = 1,
CAD = 1,
HKD = 4,
INR = 1,
MXN = 10,
NZD = 1,
SGD = 1,
GBP = 1,
USD = 1,
JPY = 100
}
7 changes: 7 additions & 0 deletions src/discord/core/open-exchange-rates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface OpenExchangeRates {
timestamp: number;
base: string;
rates: {
[key: string]: number
}
}
7 changes: 6 additions & 1 deletion src/discord/discord.module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Module, Global } from '@nestjs/common';
import { Module, Global, HttpModule } from '@nestjs/common';
import { DiscordService } from './services/discord/discord.service';
import { ProfileService } from '../shared/services/profile/profile.service';
import { TransactionService } from '../shared/services/transaction/transaction.service';
import { MoneyService } from './services/money/money.service';

@Global()
@Module({
imports: [
HttpModule
],
providers: [
MoneyService,
DiscordService,
ProfileService,
TransactionService
Expand Down
39 changes: 32 additions & 7 deletions src/discord/services/discord/discord.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { Transaction } from '../../../shared/core/transaction';
import { ProfileService } from '../../../shared/services/profile/profile.service';
import { TransactionService } from '../../../shared/services/transaction/transaction.service';
import { Args } from '../../core/args';
import { BalanceAmount } from 'src/discord/core/balance';
import { MoneyService } from '../money/money.service';

interface Keywords {
currency: string[];
Expand All @@ -30,6 +32,7 @@ export class DiscordService {
}

constructor(
private _money: MoneyService,
private _profile: ProfileService,
private _transaction: TransactionService
) {
Expand All @@ -52,6 +55,7 @@ export class DiscordService {
}

private async onMessage(message: Message): Promise<Message | Message[]> {
let channel: DMChannel
let user: Profile
const args = new Args(message.content)
console.log('Received', args)
Expand Down Expand Up @@ -87,7 +91,7 @@ export class DiscordService {
}

// Create DM channel
const channel = await message.author.createDM()
channel = await message.author.createDM()

// Send link to channel
if (channel) {
Expand All @@ -96,7 +100,11 @@ export class DiscordService {
.setTitle('NR Wallet Login Link')
.setDescription(`Hey ${message.author.toString()}, you can link your wallet here. You'll be asked to login and then you'll be directed back to our website once your account is linked! Just click the title right there.`)
.setColor(DiscordService.colours.info)
.setURL(`${config.get('wallet.endpoint')}/oauth/authorize?client_id=${config.get('wallet.client')}&redirect_uri=${config.get('wallet.redirect_uri')}&response_type=code&scope=${config.get('wallet.scope')}&state=${message.author.id},${SHA256(message.author.id, config.get('wallet.secret')).toString(enc.Hex)}`)
.setURL(
encodeURI(
`${config.get('wallet.endpoint')}/oauth/authorize?client_id=${config.get('wallet.client')}&redirect_uri=${config.get('wallet.redirect_uri')}&response_type=code&scope=${config.get('wallet.scope')}&state=${message.author.id},${SHA256(message.author.id, config.get('wallet.secret')).toString(enc.Hex)}`
)
)
.setThumbnail('https://glamsquad.sgp1.cdn.digitaloceanspaces.com/SocialHub/default/images/Logo_Transparent%20White.png')
)

Expand All @@ -117,10 +125,11 @@ export class DiscordService {
// Check if user is linked
if (!this.linkCheck(user, message)) return

// TODO: Update user balance

// Send users available and pending balance
message.reply(`Available balance: ${user.balance.available} ${user.currency}\nPending balance: ${user.balance.pending} ${user.currency}`)
// Get user balance
this._profile.balance(user).subscribe((b: BalanceAmount) => {
// Send users available and pending balance
message.reply(`Available balance: ${this._money.format(b.amount, b.currency)}`);
});
break

case '$wallet':
Expand Down Expand Up @@ -243,10 +252,26 @@ export class DiscordService {

// Send warning if guild
if (message.guild) {
message.reply(`Shhh... we shouldn't talk about that here`)
message.reply(`Shhh... we shouldn't talk about that here, I'll DM you`)
}

// TODO: Get sources and send to DM

// Create DM channel
channel = await message.author.createDM()

// Send link to channel
if (channel) {
this._profile.sources(user).subscribe(s =>
channel.send(`Hey ${message.author.toString()}, your current sources:\n${
s.reduce((str, source, i, arr) => str += `${source.card.brand[0].toUpperCase() + source.card.brand.slice(1)} ${source.type[0].toUpperCase() + source.type.slice(1)} - ${source.card.last4} (${source.card.exp_month}/${source.card.exp_year})${i !== arr.length-1 ? '\n':''}`, '')
}`)
)
return
}

// Send error if no channel
message.reply(`Hey uhm, I couldn't DM you, can you make sure I'm not blocked or anything?`)
break
}
}
Expand Down
71 changes: 71 additions & 0 deletions src/discord/services/money/money.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Injectable, HttpService } from '@nestjs/common';
import Decimal from 'decimal.js';
import * as fx from 'money';
import { Observable, timer } from 'rxjs';
import { map, share, mergeMap } from 'rxjs/operators';
import config from 'config';
import { OpenExchangeRates } from 'src/discord/core/open-exchange-rates';
import { CurrencyDecimalPlaces } from 'src/discord/core/currency-decimal-places.enum';

@Injectable()
export class MoneyService {
constructor(private http: HttpService) {
timer(0, 1000 * 60 * 60).pipe(
mergeMap(() => this.updateRates()),
share()
);
}

// Take base currency and convert to a displayable decimal
format(amount: number, currency: string): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency
}).format(this.compress(amount, currency));
}

// Take the number input and convert to base currency
unformat(amount: number, currency: string): number {
return this.uncompress(amount, currency);
}

// Compress format to decimal amount
compress(amount: number, currency: string): number {
return new Decimal(amount).div(new Decimal(10).pow(CurrencyDecimalPlaces[currency.toUpperCase()])).toNumber();
}

// Uncompress format to base denomination
uncompress(amount: number, currency: string): number {
return new Decimal(amount).times(new Decimal(10).pow(CurrencyDecimalPlaces[currency.toUpperCase()])).toNumber();
}

// Take a base currency and convert to a secondary currency
convert(amount: number, from: string, to: string): Observable<number> {
return this.updateRates().pipe(
// Compress in base currency, convert to fx and uncompress using fx
map(() => Math.round(
this.uncompress(
fx(this.compress(amount, from)).convert({ from: from.toUpperCase(), to: to.toUpperCase() }),
from
)
))
)
}

// API: Update exchange rates
updateRates(): Observable<OpenExchangeRates> {
return this.http.get<OpenExchangeRates>(
`${config.get('fx.url')}`, {
params: { app_id: config.get('fx.client_id') }
}
).pipe(
map(d => d.data),
map((data: OpenExchangeRates) => {
console.log('Open exchange rates:', data)
fx.base = data.base;
fx.rates = data.rates;
return data
})
)
}
}
19 changes: 11 additions & 8 deletions src/shared/core/profile.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { HttpService } from "@nestjs/common";

export class Profile {
public token: string;
public balance: {
available: number;
pending: number;
}
public currency: string;
public sources: any;
public id: string;
public linked = false;

constructor (data?: object) {
constructor (data: {id?: string; token?: string; currency?: string}, private readonly http?: HttpService) {
Object.assign(this, data)
this.linked = !!this.token
}

get linked(): boolean {
return !!this.token;
}

set linked(linked: boolean) {
return
}
}
Loading

0 comments on commit 42705d5

Please sign in to comment.