Skip to content

Commit

Permalink
Merge pull request #6 from proyecto26/feat/5-add-history-of-transacti…
Browse files Browse the repository at this point in the history
…ons-from-balance-page

add transaction history section from balance page
  • Loading branch information
jdnichollsc authored Feb 20, 2024
2 parents 2243ca9 + 9c0fc4c commit a91bbc8
Show file tree
Hide file tree
Showing 9 changed files with 446 additions and 29 deletions.
62 changes: 60 additions & 2 deletions apps/webapp/src/app/pages/account/account.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ <h2 id="recent-heading" class="sr-only">
</h2>

<div class="space-y-20">
<div *ngIf="(account$ | async) as account">
<div *ngIf="(account$ | async) as account; else elseAccount">
<div
class="rounded-lg bg-gray-50 px-4 py-6 sm:flex sm:items-center sm:justify-between sm:space-x-6 sm:px-6 lg:space-x-8">
<dl
class="flex-auto space-y-6 divide-y divide-gray-200 text-sm text-gray-600 sm:grid sm:grid-cols-3 sm:gap-x-6 sm:space-y-0 sm:divide-y-0 lg:w-1/2 lg:flex-none lg:gap-x-8">
<div class="flex justify-between sm:block">
<dd class="sm:mt-1">
<img [src]="account.info?.image" alt="account.info?.name || token"
<img [src]="account.info.image" alt="account.info.name"
class="h-16 w-16 rounded object-cover object-center" />
</dd>
</div>
Expand All @@ -38,6 +38,64 @@ <h2 id="recent-heading" class="sr-only">
</div>
</dl>
</div>

<table class="mt-4 w-full text-gray-500 sm:mt-6">
<caption class="sr-only">Transactions</caption>
<thead class="sr-only text-left text-sm text-gray-500 sm:not-sr-only">
<tr>
<th scope="col" class="py-3 pr-8 font-normal sm:w-2/5 lg:w-1/3">
Type
</th>
<th scope="col" class="hidden w-1/5 py-3 pr-8 font-normal sm:table-cell">
Memo
</th>
<th scope="col" class="hidden py-3 pr-8 font-normal sm:table-cell">
Amount
</th>
<th scope="col" class="w-0 py-3 text-right font-normal">
Date
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 border-b border-gray-200 text-sm sm:border-t">
<tr *ngFor="let transaction of transactions()">
<td class="py-6 pr-8">
<div class="flex items-center">
<div>
<div class="font-medium text-gray-900">
{{ transaction.type }}
</div>
</div>
</div>
</td>
<td class="hidden py-6 pr-8 sm:table-cell">
{{ transaction.memo }}
</td>
<td class="hidden py-6 pr-8 sm:table-cell">
{{ transaction.amount }}
</td>
<td class="py-6 text-right">
{{ transaction.timestamp | date: 'short' }}
</td>
</tr>
</tbody>
</table>

</div>

<ng-template #elseAccount>
<p class="text-gray-500">
Please connect your wallet to view your account details.
</p>
</ng-template>

<div *ngIf="!!error && error().length > 0" class="mt-4">
<div class="text-red-600">
{{ error() }}
</div>
<button (click)="loadTransactions()" class="text-sm font-medium text-indigo-600 hover:text-indigo-500">
Try again
</button>
</div>
</div>
</section>
Expand Down
17 changes: 13 additions & 4 deletions apps/webapp/src/app/pages/account/account.page.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, inject } from '@angular/core';
import { Component, inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';

import { LayoutContainerComponent } from '../../containers/layout/layout-container.component';
Expand All @@ -12,8 +12,17 @@ import { WalletStore } from '../../store';
styleUrl: './account.page.css',
providers: [WalletStore],
})
export class AccountPage {

export class AccountPage implements OnInit {
readonly walletStore = inject(WalletStore);
readonly account$ = this.walletStore.account()
readonly account$ = this.walletStore.account();
readonly transactions = this.walletStore.transactions;
readonly error = this.walletStore.error;

ngOnInit() {
this.loadTransactions();
}

loadTransactions() {
this.walletStore.loadTransactions(10);
}
}
2 changes: 1 addition & 1 deletion apps/webapp/src/app/store/wallet/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './store'
export * from './model'
export * from './models'
export * from './service'
16 changes: 0 additions & 16 deletions apps/webapp/src/app/store/wallet/model.ts

This file was deleted.

110 changes: 110 additions & 0 deletions apps/webapp/src/app/store/wallet/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
export type TokenBalanceResponse = {
success: boolean;
message: string;
result: {
address: string;
balance: number;
associated_account: string;
info: {
name: string;
symbol: string;
image: string;
decimals: number;
};
isFrozen: false;
};
};

export type TransactionType = 'SOL_TRANSFER' | 'TOKEN_TRANSFER';

export type TransactionProtocol = {
address: string;
name: string;
};

export type TransactionAction = {
info: {
sender: string;
receiver: string;
amount: number;
message?: string;
};
source_protocol: string;
type: TransactionType;
};

export type TransactionRawMeta = {
computeUnitsConsumed: number;
err: null | Error;
fee: number;
innerInstructions: [];
logMessages: Array<string>;
postBalances: Array<number>;
postTokenBalances: [];
preBalances: Array<number>;
preTokenBalances: [];
rewards: [];
status: {
Ok: null;
};
};

export type AccountKey = {
pubkey: string;
signer: boolean;
source: string;
writable: boolean;
};

export type TransactionInstruction = {
parsed: {
info: {
destination: string;
lamports: number;
source: string;
};
type: string;
};
program: string;
programId: string;
};

export type Transaction = {
timestamp: string;
fee: number;
fee_payer: string;
signers: Array<string>;
signatures: Array<string>;
protocol: TransactionProtocol;
type: TransactionType;
actions: Array<TransactionAction>;
raw: {
blockTime: number;
meta: TransactionRawMeta;
slot: number;
transaction: {
message: {
accountKeys: Array<AccountKey>;
addressTableLookups: null;
instructions: Array<TransactionInstruction>;
recentBlockhash: string;
};
signatures: Array<string>;
};
version: string;
};
}

export type TransactionHistoryResponse = {
success: boolean;
message: string;
result: Array<Transaction>;
};

export type Transactions = Array<{
timestamp: Date;
type: 'transfer' | 'unknown';
memo?: string
amount?: number,
sign?: -1 | 1,
}>;
60 changes: 57 additions & 3 deletions apps/webapp/src/app/store/wallet/service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { map, of } from 'rxjs';
import { getAssociatedTokenAddressSync } from '@solana/spl-token';
import { map, of, timeout } from 'rxjs';
import { PublicKey } from '@solana/web3.js';

import { environment } from '../../../environments/environment';
import { TokenBalanceResponse } from './model';
import {
TokenBalanceResponse,
Transaction,
TransactionHistoryResponse,
Transactions,
} from './models';

const isInvalidTransaction = (transaction: Transaction) => {
return (
transaction.type !== 'TOKEN_TRANSFER' || transaction.actions.length !== 2
);
};

@Injectable({ providedIn: 'root' })
export class ShyftApiService {
Expand All @@ -28,6 +41,47 @@ export class ShyftApiService {
'x-api-key': environment.shyftApiKey,
},
})
.pipe(map((res) => res.result));
.pipe(
timeout(5000),
map((res) => res.result));
}

getTransactions(publicKey: PublicKey, limit = 100) {
const account = getAssociatedTokenAddressSync(
new PublicKey(environment.mintUSDC),
publicKey
);
const url = new URL('/sol/v1/transaction/history', environment.shyftApiUrl);
url.searchParams.append('network', environment.walletNetwork);
url.searchParams.append('account', account.toBase58());
url.searchParams.append('tx_num', limit.toString());

const wallet = publicKey.toBase58();

return this.http
.get<TransactionHistoryResponse>(url.toString(), {
headers: {
'x-api-key': environment.shyftApiKey,
},
})
.pipe(
timeout(5000),
map((res) =>
res.result.map<Transactions[0]>((transaction) => ({
...(isInvalidTransaction(transaction)
? {
timestamp: new Date(transaction.timestamp),
type: 'unknown',
}
: {
timestamp: new Date(transaction.timestamp),
memo: transaction.actions[1].info.message,
amount: transaction.actions[1].info.amount,
sign: transaction.actions[0].info.sender === wallet ? -1 : 1,
type: 'transfer',
}),
}))
)
);
}
}
42 changes: 39 additions & 3 deletions apps/webapp/src/app/store/wallet/store.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import { computed, inject } from '@angular/core';
import { signalStore, withComputed, withState } from '@ngrx/signals';
import { tapResponse } from '@ngrx/operators';
import {
patchState,
signalStore,
withComputed,
withMethods,
withState,
} from '@ngrx/signals';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { WalletStore as WalletAdapterStore } from '@heavy-duty/wallet-adapter';
import { combineLatest, of, pipe, switchMap, tap } from 'rxjs';

import { ShyftApiService } from './service';
import { switchMap } from 'rxjs';
import { Transactions } from './models';

type WalletState = {
isLoading: boolean;
wallet: WalletAdapterStore;
transactions: Transactions;
isLoading?: boolean;
error?: string;
};

Expand All @@ -16,6 +26,8 @@ export const WalletStore = signalStore(
() =>
<WalletState>{
wallet: inject(WalletAdapterStore),
transactions: [],
error: '',
}
),
withComputed((store, walletService = inject(ShyftApiService)) => ({
Expand All @@ -28,5 +40,29 @@ export const WalletStore = signalStore(
)
);
}),
})),
withMethods((store, walletService = inject(ShyftApiService)) => ({
loadTransactions: rxMethod<number>(
pipe(
tap(() => patchState(store, { isLoading: true })),
switchMap((limit) =>
combineLatest([store.wallet().publicKey$, of(limit)])
),
switchMap(([publicKey, limit]) => {
if (!publicKey) return [];
return walletService.getTransactions(publicKey, limit).pipe(
tapResponse({
next: (transactions) => patchState(store, { transactions }),
error: (error: Error) => {
console.error('error', error);
patchState(store, { error: error.message });
},
complete: () =>
patchState(store, { isLoading: false, error: '' }),
})
);
})
)
),
}))
);
Loading

0 comments on commit a91bbc8

Please sign in to comment.