Commit 62df98c9 authored by John Doe's avatar John Doe
Browse files

Upload Frontend.

parent 8b6a1aef
Pipeline #5 failed with stages
in 0 seconds
export * from './pipes.module';
export * from './short-address.pipe';
import { Pipe, PipeTransform } from '@angular/core';
import { normalizeBN, BigNumberValue } from '../helpers';
@Pipe({
name: 'normalizeBN'
})
export class NormalizeBnPipe implements PipeTransform {
public transform(value: BigNumberValue = '0', decimals: number = 18): number {
return Number(normalizeBN(value, decimals).dp(5).toString(10));
}
}
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NormalizeBnPipe } from './normalize-bn.pipe';
import { ShortAddressPipe } from './short-address.pipe';
@NgModule({
declarations: [
ShortAddressPipe,
NormalizeBnPipe,
],
exports: [
ShortAddressPipe,
NormalizeBnPipe,
],
imports: [
CommonModule,
]
})
export class PipesModule {
}
import { Pipe, PipeTransform } from '@angular/core';
import { shortAddress, isTxHash, shortTxHash } from '../helpers';
@Pipe({
name: 'shortAddress'
})
export class ShortAddressPipe implements PipeTransform {
public transform(address: string): string {
return isTxHash(address) ? shortTxHash(address) : shortAddress(address);
}
}
import { Action, createReducer, on } from '@ngrx/store';
import {
GetAllowanceSuccess,
GetBalancesSuccess,
GetLatestPriceSuccess,
GetXFTBalanceSuccess,
SetAccountAddress,
SetRouterParams,
} from './actions';
import { RouterState } from './index';
import { OutputParamsBalance } from './interfaces/contract-service-methods';
import { TOKENS_MAP } from './helpers';
import { TokensName } from './enums';
export const coreKey = 'context';
export interface CoreState extends RouterState {
accountAddress: string;
xftBalance: string;
allowance: string;
balances: OutputParamsBalance[];
assetPrice: {
price: string;
decimals: number;
};
}
export const initialState: CoreState = {
accountAddress: undefined,
balances: undefined,
xftBalance: undefined,
allowance: undefined,
assetPrice: undefined,
params: undefined,
queryParams: undefined,
url: undefined,
};
const _coreReducer = createReducer(
initialState,
on(SetRouterParams, (state, { url, params, queryParams }): CoreState => {
return { ...state, url, params, queryParams };
}),
on(SetAccountAddress, (state, { accountAddress }): CoreState => {
return { ...state, accountAddress };
}),
on(GetXFTBalanceSuccess, (state, { xftBalance }): CoreState => {
TOKENS_MAP[TokensName.XFT].balance = xftBalance;
return { ...state, xftBalance };
}),
on(GetLatestPriceSuccess, (state, { assetPrice }): CoreState => {
return { ...state, assetPrice };
}),
on(GetAllowanceSuccess, (state, { allowance }): CoreState => {
return { ...state, allowance };
}),
on(GetBalancesSuccess, (state, { balances }): CoreState => {
balances?.forEach(({ asset }) => {
if (asset.symbol && TOKENS_MAP[asset.symbol]) {
TOKENS_MAP[asset.symbol].balance = asset.amount;
}
});
return { ...state, balances };
}),
);
export function reducer(state: CoreState, action: Action): CoreState {
return _coreReducer(state, action);
}
import { Params, RouterStateSnapshot } from '@angular/router';
import { RouterStateSerializer } from '@ngrx/router-store';
import { RouterParams, RouterState, initialRouterParamsState } from './index';
export class RouterParamsSerializer implements RouterStateSerializer<RouterState> {
public serialize({ url, root }: RouterStateSnapshot): RouterState {
let route = root;
let queryParams: Params = {};
let params: RouterParams = initialRouterParamsState;
while (route.firstChild) {
route = route.firstChild;
params = { ...params, ...parseParams(route.params) };
queryParams = { ...queryParams, ...parseParams(route.queryParams) };
}
return { url, params, queryParams };
}
}
function parseParams(params: Params): Params {
const obj: Params = {};
for (const param in params) {
if (params.hasOwnProperty(param)) {
obj[param] = !params[param].startsWith('0x') && !isNaN(+params[param]) ? +params[param] : params[param];
}
}
return obj;
}
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { coreKey, CoreState } from './reducer';
import { normalizeBN, TOKENS_MAP, valueToBigNumber } from './helpers';
import { Tokens, TokensName } from './enums';
export const selectContextState = createFeatureSelector<CoreState>(coreKey);
export const selectContext = createSelector(
selectContextState,
(context) => context,
);
export const selectAccountAddress = createSelector(
selectContext,
(state) => state.accountAddress,
);
export const selectXFTBalance = createSelector(
selectContext,
(state) => state.xftBalance,
);
export const selectAssetPrice = createSelector(
selectContext,
(state) => state.assetPrice,
);
export const selectAllowance = createSelector(
selectContext,
(state) => state.allowance,
);
export const selectBalances = createSelector(
selectContext,
(state) => {
return state?.balances?.map((balance) => {
return {
...TOKENS_MAP[balance.asset.symbol],
...balance,
};
});
},
);
export const selectAssets = createSelector(
selectBalances,
(balances) => {
return Object.values(TOKENS_MAP)
.filter(({ symbol }) => symbol !== TokensName.XFT)
.map((token) => {
const balance = balances?.find((b) => b.asset.symbol === token.symbol);
return {
...token,
asset: {
symbol: token.symbol,
amount: balance ? balance.asset.amount : '0',
USD: balance ? balance.asset.USD : '0',
XFT: balance ? balance.asset.XFT : '0',
},
commitments: balance ? balance.commitments : [],
};
});
},
);
export const selectXFTBalancesAmount = createSelector(
selectBalances,
(balances) => {
const amount = balances?.map((balance) => balance.asset.XFT)
.reduce((prev, cur) => {
prev = prev.plus(cur);
return prev;
}, valueToBigNumber(0));
return normalizeBN(amount, 18).dp(2).toString(10);
},
);
export const selectTokenBySymbol = (symbol: TokensName) => {
return createSelector(
selectBalances,
(balances) => balances?.find(({ asset }) => symbol === asset.symbol),
);
};
export const selectChartData = createSelector(
selectBalances,
(balances) => {
const data = balances?.map((b) => {
return normalizeBN(b.asset.amount, 18)
.dp(5)
.toNumber();
});
return {
labels: Object.values(Tokens).map((symbol) => ` ${ symbol }`),
datasets: [
{
data,
label: '',
borderWidth: 0,
backgroundColor: [
'#5800B0',
'#6B39FA',
'#A98CFF',
'#D1C1FE',
],
hoverBackgroundColor: [
'#5800B0',
'#6B39FA',
'#A98CFF',
'#D1C1FE',
],
},
],
};
},
);
import { Store } from '@ngrx/store';
import { firstValueFrom } from 'rxjs';
import { environment } from '../../../environments/environment';
import { State } from '../index';
import { CallReturn, MethodCallback, SendReturn } from '../interfaces/callback';
import { selectAccountAddress } from '../selectors';
export abstract class BaseService {
protected accountAddress!: string;
protected constructor(
protected readonly store: Store,
) {
}
protected apiUrl(): string {
return environment.apiUrl;
}
protected async call<T = any>(method: CallReturn<T>, cb: MethodCallback = null): Promise<T> {
const from = await firstValueFrom(this.store.select(selectAccountAddress));
return await method.call({ from }, cb);
}
protected async send<T = any>(method: SendReturn<T>, cb: MethodCallback = null): Promise<T> {
const from = await firstValueFrom(this.store.select(selectAccountAddress));
const gas = await method.estimateGas({ from });
return await method.send({ from, gas }, cb);
}
}
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { isAddress } from '@ethersproject/address';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { AbiItem } from 'web3-utils';
import { environment } from '../../../environments/environment';
import { BalanceContract, RecipientCommitment, SchnorrSignatureSet } from '../interfaces/balance-contract';
import { MethodCallback } from '../interfaces/callback';
import {
ConfigData,
InitData, InputParamsBalance, InputParamsExchange,
InputParamsPrice, OutputParamsBalance,
} from '../interfaces/contract-service-methods';
import { PriceContract } from '../interfaces/price-contract';
import { TokenContract } from '../interfaces/token-contract';
import { BaseService } from './base.service';
import { BALANCE_CONTRACT_ABI, PRICE_CONTRACT_ABI, TOKEN_CONTRACT_ABI } from './contracts-abi';
import { WalletService } from './wallet.service';
import { Contract } from 'web3-eth-contract';
import { BigNumberValue, valueToBigNumber } from '../helpers';
import { TokensName } from '../enums';
const CONTRACT_MAP: Map<string, Contract> = new Map<string, Contract>([]);
@Injectable({
providedIn: 'root'
})
export class ContractService extends BaseService {
constructor(
store: Store,
private readonly _http: HttpClient,
private readonly _walletService: WalletService
) {
super(store);
}
public getPrice(payload: InputParamsPrice): Observable<any> {
return this._http.post<any>(this.apiUrl('getPrice'), payload);
}
public exchange(payload: InputParamsExchange): Observable<any> {
return this._http.post<any>(this.apiUrl('exchange'), payload);
}
public balances(payload: InputParamsBalance): Observable<OutputParamsBalance[]> {
return this._http.post<OutputParamsBalance[]>(this.apiUrl('balances'), payload);
}
public init(payload: InitData): Observable<any> {
return this._http.post<any>(this.apiUrl('init'), payload);
}
public config(payload: ConfigData): Observable<any> {
return this._http.post<any>(this.apiUrl('config'), payload);
}
public async approve(amount: string, cb: MethodCallback = null): Promise<any> {
const { methods } = this._getXFTContract();
return this.send(methods.approve(environment.balanceContractAddress, amount), cb);
}
public async allowance(
owner: string,
spender: string,
cb: MethodCallback = null,
): Promise<any> {
const { methods } = this._getXFTContract();
return this.call(methods.allowance(owner, spender), cb);
}
public async deposit(
address: string,
amount: BigNumberValue,
currency: TokensName,
cb: MethodCallback = null,
): Promise<any> {
const { methods } = this._getBalanceContract();
const {
amountIn,
assetEnum: asset,
commitment,
message,
aggregatePubKey: publicKey,
aggregaterR: ecR,
aggregateS: s,
} = await this._http.post<any>(this.apiUrl('deposit'), {
address,
amount: valueToBigNumber(amount).toNumber(),
currency
}).toPromise();
const recipientCommitment: RecipientCommitment = { commitment, asset };
const schnorrSignatureSet: SchnorrSignatureSet = { message, publicKey, ecR, s };
return this.send(methods.deposit(amountIn, amount, recipientCommitment, schnorrSignatureSet), cb);
}
public async depositFill(payload: { address: string, hashTx: string }): Promise<any> {
return this._http.post<any>(this.apiUrl('deposit', 'fill'), payload).toPromise();
}
public async withdraw(
address: string,
amount: BigNumberValue,
commitmentId: BigNumberValue,
cb: MethodCallback = null,
): Promise<any> {
const {
message,
aggregatePubKey: publicKey,
aggregaterR: ecR,
aggregateS: s,
} = await this._http.post<any>(this.apiUrl('withdraw'), {
address,
amount: valueToBigNumber(amount).toNumber(),
commitmentId: Number(commitmentId),
}).toPromise();
const schnorrSignatureSet: SchnorrSignatureSet = { message, publicKey, ecR, s };
const { methods } = this._getBalanceContract();
return this.send(methods.withdraw(amount, Number(commitmentId), schnorrSignatureSet), cb);
}
public async withdrawFill(payload: { address: string, hashTx: string }): Promise<any> {
return this._http.post<any>(this.apiUrl('withdraw', 'fill'), payload).toPromise();
}
public async groupCommitment(
address: string,
commitmentsId: BigNumberValue[],
cb: MethodCallback = null,
): Promise<any> {
const {
assetEnum,
commitment,
pkSum,
} = await this._http.post<any>(this.apiUrl('group'), {
address,
commitmentsId,
}).toPromise();
const { methods } = this._getBalanceContract();
return this.send(methods.groupCommitment(commitmentsId, assetEnum, commitment, pkSum), cb);
}
public async groupCommitmentsSpent(
address: string,
hashTx: string,
commitmentsId: BigNumberValue[],
cb: MethodCallback = null,
): Promise<any> {
return await this._http.post<any>(this.apiUrl('group', 'spent'), {
address,
hashTx,
commitmentsId,
}).toPromise();
}
public async groupCommitmentFill(payload: { address: string, hashTx: string }): Promise<any> {
return await this._http.post<any>(this.apiUrl('group', 'fill'), payload).toPromise();
}
public async xftBalance(address: string): Promise<string> {
const { methods } = this._getXFTContract();
return this.call(methods.balanceOf(address));
}
public async getLatestPrice(aggregator: number): Promise<{ price: string, decimals: number }> {
const { methods } = this._getPriceContract();
const latestPrice = await this.call(methods.getLatestPrice(aggregator));
const [price, decimals] = Object.values(latestPrice);
return { price, decimals: Number(decimals) };
}
private _getXFTContract(): TokenContract {
return this._getContractInstance(TOKEN_CONTRACT_ABI, environment.tokenContractAddress);
}
private _getPriceContract(): PriceContract {
return this._getContractInstance(PRICE_CONTRACT_ABI, environment.priceContractAddress);
}
private _getBalanceContract(): BalanceContract {
return this._getContractInstance(BALANCE_CONTRACT_ABI, environment.balanceContractAddress);
}
private _getContractInstance<T extends Contract>(abi: AbiItem[], address: string): T {
if (!isAddress(address)) {
throw new Error('Contract address is not valid');
}
if (CONTRACT_MAP.has(address)) {
return CONTRACT_MAP.get(address) as T;
}
const contractInstance = new this._walletService.web3.eth.Contract(abi, address);
CONTRACT_MAP.set(address, contractInstance);
return contractInstance as T;
}
protected override apiUrl(...path: string[]): string {
let url = `${ super.apiUrl() }`;
if (path && Array.isArray(path)) {
url = path.reduce((cur, prev) => {
cur += `/${ prev }`;
return cur;
}, url);
}
return url;
}
}
This diff is collapsed.
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import WalletConnect from '@walletconnect/web3-provider';
import { BehaviorSubject, Subject } from 'rxjs';
import Web3 from 'web3';
import Web3Modal from 'web3modal';
import { SetAccountAddress } from '../actions';
import { EthEvents, EthMethods } from '../enums';
import {
ConnectInfo,
EthChainParams,
Ethereum,
ProviderMessage,
ProviderRpcError,
} from '../interfaces/ethereum';
import { BaseService } from './base.service';
@Injectable({
providedIn: 'root'
})
export class WalletService extends BaseService {
public get web3(): Web3 {
return this._web3;
}
public get web3Modal(): Web3Modal {
return this._web3Modal;
}
private _web3: Web3;
private _web3Modal: Web3Modal;
private _accountsChanged$: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]);
private _chainChanged$: BehaviorSubject<string | null> = new BehaviorSubject<string>(null);
private _networkChanged$: BehaviorSubject<number | null> = new BehaviorSubject<number | null>(null);
private _balanceChanged$: BehaviorSubject<string | null> = new BehaviorSubject<string>(null);
private _connectChanged$: Subject<ConnectInfo> = new Subject<ConnectInfo>();
private _disconnectChanged$: Subject<ProviderRpcError> = new Subject<ProviderRpcError>();
private _messageChanged$: Subject<ProviderMessage> = new Subject<ProviderMessage>();
constructor(
store: Store,
) {
super(store);
}
public async initialize(): Promise<void> {
const providerOptions = {
walletconnect: {
package: WalletConnect, // required
options: {
infuraId: 'INFURA_ID' // required
},
},
};
this._web3Modal = new Web3Modal({
network: 'mainnet', // optional
cacheProvider: true, // optional
providerOptions, // required
theme: {
background: '#FFFFFF',
main: '#1B0D3D',
secondary: '#1B0D3D',
border: 'rgba(88, 0, 176, 0.05)',
hover: 'rgba(88, 0, 176, 0.15)',
},
});
if (this.web3Modal.cachedProvider) {
return this.connect();
}
}
public async connect(): Promise<void> {
try {
const provider = await this._web3Modal.connect();
await this._subscribeProvider(provider);
this._web3 = new Web3(provider);
const [accountAddress] = await this._web3.eth.getAccounts();
this.store.dispatch(SetAccountAddress({ accountAddress }));
} catch (error) {
console.log(error);
}
}
public clearCachedProvider(): void {
this._web3Modal.clearCachedProvider();
}
public async switchEthereumChain(chainId: string): Promise<void> {
await this._request(EthMethods.SwitchEthereumChain, [{ chainId }]);
}
public async addEthereumChain(params: Partial<EthChainParams>): Promise<void> {
await this._request(EthMethods.AddEthereumChain, [params]);
}
private _request<T = any>(
method: EthMethods,
params?: unknown[] | Record<string, unknown>,
): Promise<T> {
const eth = (window as any).ethereum as Ethereum;
return eth.request({ method, params });
}
private async _subscribeProvider(provider: any) {
if (!provider.on) {
return;
}
provider.on(EthEvents.ChainChanged, (chainId: string) => {
console.log(EthEvents.ChainChanged, ':', chainId);
// It's recommended to reload the page on chain changes, unless you have good reason not to.
window.location.reload();
});
provider.on(EthEvents.AccountsChanged, ([accountAddress]: string[]) => {
this.store.dispatch(SetAccountAddress({ accountAddress }));
});
provider.on(EthEvents.Connect, (connectInfo: ConnectInfo) => {
console.log(EthEvents.Connect, ':', connectInfo);
this._connectChanged$.next(connectInfo);
});
provider.on(EthEvents.Disconnect, (error: ProviderRpcError) => {
console.log(EthEvents.Disconnect, ':', error);
this._disconnectChanged$.next(error);
});
}
}
import { Tokens, TokensName } from '../enums';
export type TokenType = {
color: string;
icon: string;
name: string;
balance?: string;
symbol: TokensName;
zkSymbol: Tokens;
}
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ErrorRoutingModule { }
<p>error works!</p>
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ErrorComponent } from './error.component';
describe('ErrorComponent', () => {
let component: ErrorComponent;
let fixture: ComponentFixture<ErrorComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ErrorComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ErrorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'os-error',
templateUrl: './error.component.html',
styleUrls: ['./error.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ErrorComponent {
}
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ErrorRoutingModule } from './error-routing.module';
import { ErrorComponent } from './error.component';
@NgModule({
declarations: [
ErrorComponent
],
imports: [
CommonModule,
ErrorRoutingModule
]
})
export class ErrorModule { }
<div class="assets">
<div class="assets__search p-input-icon-left">
<i class="pi pi-search"></i>
<input type="text"
pInputText
placeholder="Symbol, name or address"/>
</div>
<div *ngFor="let asset of assets"
(click)="onSelectAsset(asset)"
class="assets__item">
<div class="assets__icon">
<img [src]="asset.icon" [alt]="asset.name">
</div>
<div class="assets__content">
<div class="assets__name">{{ asset.name }}</div>
<div class="assets__symbol">{{ asset.zkSymbol }}</div>
</div>
</div>
</div>
@import "~src/styles/variables";
:host {
display: block;
}
.assets {
display: flex;
flex-direction: column;
& &__search {
width: 100%;
margin-bottom: 12px;
> .p-inputtext {
width: 100%;
}
}
& &__item {
display: flex;
background: rgba(88, 0, 176, 0.05);
padding: 12px;
gap: 16px;
border-radius: 4px;
&:not(:last-of-type) {
margin-bottom: 12px;
}
&:hover {
cursor: pointer;
background: rgba(88, 0, 176, 0.15);
}
&:active {
background: rgba(88, 0, 176, 0.25);
}
}
& &__icon {
width: 44px;
height: 44px;
> img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
& &__content {
display: flex;
flex-direction: column;
}
& &__name,
& &__symbol {
color: #1B0D3D;
}
& &__name {
font-weight: 400;
font-size: 14px;
line-height: 22px;
opacity: 0.7;
}
& &__symbol {
font-weight: 600;
font-size: 16px;
line-height: 26px;
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment