diff --git a/.vscode/settings.json b/.vscode/settings.json index 7557d8f4..04822704 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,5 +16,6 @@ }, "[rust]": { "editor.defaultFormatter": "rust-lang.rust" - } + }, + "cSpell.words": ["sidenav"] } diff --git a/apps/broadcaster/src/app/events/events.gateway.ts b/apps/broadcaster/src/app/events/events.gateway.ts index 19656498..ebe0ac3b 100644 --- a/apps/broadcaster/src/app/events/events.gateway.ts +++ b/apps/broadcaster/src/app/events/events.gateway.ts @@ -8,8 +8,12 @@ import { WebSocketGateway, WebSocketServer, } from '@nestjs/websockets'; -import { Connection, TransactionSignature } from '@solana/web3.js'; +import { Connection, Transaction, TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { v4 as uuid } from 'uuid'; import WebSocket, { Server } from 'ws'; +import { environment } from '../../environments/environment'; +import { EventsService } from './events.service'; @WebSocketGateway({ cors: { @@ -20,26 +24,11 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { private readonly _logger = new Logger(EventsGateway.name); @WebSocketServer() private readonly _server: Server; - private readonly _topics = new Map>(); - private readonly _connection = new Connection('http://localhost:8899'); + private readonly _connection = new Connection(environment.rpcUrl, { + confirmTransactionInitialTimeout: 120_000, // timeout for 2 minutes ~blockhash duration + }); - private broadcastTransaction( - event: string, - signature: TransactionSignature, - topic: string - ) { - this._topics.get(topic)?.forEach((client) => - client.send( - JSON.stringify({ - event, - data: { - signature, - topic, - }, - }) - ) - ); - } + constructor(private readonly _eventsService: EventsService) {} handleConnection( @ConnectedSocket() @@ -49,7 +38,10 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { `Client connected. [${this._server.clients.size} clients connected]` ); - this._topics.set('*', new Set([...(this._topics.get('*') ?? []), client])); + this._eventsService.dispatch({ + type: 'CLIENT_CONNECTED', + payload: client, + }); } handleDisconnect( @@ -60,16 +52,9 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { `Client disconnected. [${this._server.clients.size} clients connected]` ); - this._topics.forEach((_, topic) => { - const clients = [...(this._topics.get(topic) ?? [])].filter( - (ws) => ws !== client - ); - - if (clients.length === 0) { - this._topics.delete(topic); - } else { - this._topics.set(topic, new Set(clients)); - } + this._eventsService.dispatch({ + type: 'CLIENT_DISCONNECTED', + payload: client, }); } @@ -77,33 +62,51 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { onSubscribe( @ConnectedSocket() client: WebSocket, - @MessageBody() topic: string + @MessageBody() + { + topicName, + correlationId, + }: { + topicName: string; + correlationId: string; + } ) { - this._logger.log(`Client subscribed to [${topic}].`); - - this._topics.set( - topic, - new Set([...(this._topics.get(topic) ?? []), client]) - ); + this._logger.log(`Client subscribed to [${topicName}].`); + + this._eventsService.dispatch({ + type: 'CLIENT_SUBSCRIBED', + payload: { + client, + topicName, + subscriptionId: uuid(), + correlationId, + }, + }); } @SubscribeMessage('unsubscribe') onUnsubscribe( @ConnectedSocket() client: WebSocket, - @MessageBody() topic: string - ) { - this._logger.log(`Client unsubscribed from [${topic}].`); - - const clients = [...(this._topics.get(topic) ?? [])].filter( - (ws) => ws !== client - ); - - if (clients.length === 0) { - this._topics.delete(topic); - } else { - this._topics.set(topic, new Set(clients)); + @MessageBody() + { + topicName, + subscriptionId, + }: { + topicName: string; + subscriptionId: string; } + ) { + this._logger.log(`Client unsubscribed from [${topicName}].`); + + this._eventsService.dispatch({ + type: 'CLIENT_UNSUBSCRIBED', + payload: { + client, + topicName, + subscriptionId, + }, + }); } @SubscribeMessage('transaction') @@ -111,38 +114,102 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { @MessageBody() { transactionSignature, - topic, + transaction, + topicNames, }: { transactionSignature: TransactionSignature; - topic: string; + transaction: Transaction; + topicNames: List; } ) { - this._logger.log(`Transaction received [${transactionSignature}].`); - - await this._connection.confirmTransaction( - transactionSignature, - 'confirmed' + this._logger.log( + `Transaction received [${transactionSignature}]. (${topicNames.join( + ', ' + )})` ); - this._logger.log(`Transaction confirmed [${transactionSignature}].`); + this._eventsService.dispatch({ + type: 'TRANSACTION_RECEIVED', + payload: { + topicNames, + transactionStatus: { + transaction, + signature: transactionSignature, + timestamp: Date.now(), + }, + }, + }); - this.broadcastTransaction( - 'transactionConfirmed', - transactionSignature, - topic - ); + try { + await this._connection.confirmTransaction( + transactionSignature, + 'confirmed' + ); - await this._connection.confirmTransaction( - transactionSignature, - 'finalized' - ); + this._logger.log( + `Transaction confirmed [${transactionSignature}]. (${topicNames.join( + ', ' + )})` + ); + + this._eventsService.dispatch({ + type: 'TRANSACTION_CONFIRMED', + payload: { + topicNames, + transactionStatus: { + transaction, + signature: transactionSignature, + timestamp: Date.now(), + status: 'confirmed', + }, + }, + }); - this._logger.log(`Transaction finalized [${transactionSignature}].`); + await this._connection.confirmTransaction( + transactionSignature, + 'finalized' + ); - this.broadcastTransaction( - 'transactionFinalized', - transactionSignature, - topic - ); + this._logger.log( + `Transaction finalized [${transactionSignature}]. (${topicNames.join( + ', ' + )})` + ); + + this._eventsService.dispatch({ + type: 'TRANSACTION_FINALIZED', + payload: { + topicNames, + transactionStatus: { + transaction, + signature: transactionSignature, + timestamp: Date.now(), + status: 'finalized', + }, + }, + }); + } catch (error) { + this._logger.log( + `Transaction failed [${transactionSignature}]. (${topicNames.join( + ', ' + )})` + ); + + this._eventsService.dispatch({ + type: 'TRANSACTION_FAILED', + payload: { + topicNames, + transactionStatus: { + transaction, + signature: transactionSignature, + timestamp: Date.now(), + error: { + name: 'ConfirmTransactionError', + message: error.message, + }, + }, + }, + }); + } } } diff --git a/apps/broadcaster/src/app/events/events.module.ts b/apps/broadcaster/src/app/events/events.module.ts index 2b2d1cbb..c830b2ea 100644 --- a/apps/broadcaster/src/app/events/events.module.ts +++ b/apps/broadcaster/src/app/events/events.module.ts @@ -1,7 +1,8 @@ import { Module } from '@nestjs/common'; import { EventsGateway } from './events.gateway'; +import { EventsService } from './events.service'; @Module({ - providers: [EventsGateway], + providers: [EventsService, EventsGateway], }) export class EventsModule {} diff --git a/apps/broadcaster/src/app/events/events.service.ts b/apps/broadcaster/src/app/events/events.service.ts new file mode 100644 index 00000000..9a28ec98 --- /dev/null +++ b/apps/broadcaster/src/app/events/events.service.ts @@ -0,0 +1,137 @@ +import { Injectable } from '@nestjs/common'; +import { + BehaviorSubject, + filter, + mapTo, + merge, + scan, + share, + tap, + withLatestFrom, +} from 'rxjs'; +import { initialState, reduce } from './state'; +import type { + ActionTypes, + ClientSubscribed, + State, + TransactionConfirmed, + TransactionFailed, + TransactionFinalized, + TransactionReceived, +} from './types'; + +@Injectable() +export class EventsService { + private readonly _dispatch = new BehaviorSubject(null); + readonly actions$ = this._dispatch.asObservable(); + readonly state$ = this.actions$.pipe( + scan((state, action) => { + if (action === null) { + return state; + } + + return reduce(state, action); + }, initialState), + share() + ); + + // When client subscribes send all the transactions in process + private readonly _clientSubscribed$ = this.actions$.pipe( + filter( + (action): action is ClientSubscribed => + action !== null && action.type === 'CLIENT_SUBSCRIBED' + ), + withLatestFrom(this.state$), + tap(([action, state]: [ClientSubscribed, State]) => { + const { client, topicName, subscriptionId, correlationId } = + action.payload; + const topic = state.topics.get(topicName); + + client.send( + JSON.stringify({ + event: 'message', + data: { + id: correlationId, + subscriptionId, + }, + }) + ); + + if (topic !== undefined) { + topic.transactions.forEach((transactionStatus) => { + client.send( + JSON.stringify({ + event: 'message', + data: { + topicName, + transactionStatus, + subscriptionId, + }, + }) + ); + }); + } + }), + mapTo(null) + ); + + // Whenever a transaction changes broadcast to respective topics + private readonly _handleTransaction$ = this.actions$.pipe( + filter( + (action): action is TransactionReceived => + action !== null && + (action.type === 'TRANSACTION_RECEIVED' || + action.type === 'TRANSACTION_CONFIRMED' || + action.type === 'TRANSACTION_FINALIZED' || + action.type === 'TRANSACTION_FAILED') + ), + withLatestFrom(this.state$), + tap( + ([action, state]: [ + ( + | TransactionReceived + | TransactionConfirmed + | TransactionFinalized + | TransactionFailed + ), + State + ]) => { + const { transactionStatus, topicNames } = action.payload; + + topicNames.forEach((topicName) => { + const topic = state.topics.get(topicName); + + if (topic !== undefined) { + topic.clients.forEach(({ client, subscriptions }) => { + subscriptions.forEach((subscriptionId) => { + client.send( + JSON.stringify({ + event: 'message', + data: { + topicName, + transactionStatus, + subscriptionId, + }, + }) + ); + }); + }); + } + }); + } + ), + mapTo(null) + ); + + constructor() { + merge(this._clientSubscribed$, this._handleTransaction$).subscribe( + this._dispatch + ); + + this.state$.subscribe(); + } + + dispatch(action: ActionTypes) { + this._dispatch.next(action); + } +} diff --git a/apps/broadcaster/src/app/events/state.ts b/apps/broadcaster/src/app/events/state.ts new file mode 100644 index 00000000..8def581f --- /dev/null +++ b/apps/broadcaster/src/app/events/state.ts @@ -0,0 +1,193 @@ +import { Map, Set } from 'immutable'; +import { ActionTypes, State } from './types'; + +export const initialState: State = { + topics: Map(), + clients: Map(), +}; + +export const reduce = (state: State, action: ActionTypes): State => { + switch (action.type) { + case 'CLIENT_CONNECTED': + return { + ...state, + clients: state.clients.set(action.payload, { + client: action.payload, + subscriptions: Map(), + }), + }; + case 'CLIENT_DISCONNECTED': { + const broadcasterClient = state.clients.get(action.payload); + + if (broadcasterClient === undefined) { + return state; + } + + return { + ...state, + clients: state.clients.remove(action.payload), + topics: broadcasterClient.subscriptions + .reduce( + (topics, topicSubscription) => + topics.update( + topicSubscription.topicName, + undefined, + (topic) => ({ + ...topic, + clients: topic.clients.delete(broadcasterClient.client), + }) + ), + state.topics + ) + .filter( + (topic) => topic.clients.size > 0 || topic.transactions.size > 0 + ), + }; + } + case 'CLIENT_SUBSCRIBED': { + const { client, subscriptionId, topicName } = action.payload; + + return { + ...state, + clients: state.clients.update( + client, + { + client, + subscriptions: Map([ + [subscriptionId, { topicName, subscriptionId }], + ]), + }, + (broadcasterClient) => ({ + ...broadcasterClient, + subscriptions: broadcasterClient.subscriptions.set(subscriptionId, { + subscriptionId, + topicName, + }), + }) + ), + topics: state.topics.update( + topicName, + { + topicName, + transactions: Map(), + clients: Map([ + [client, { client, subscriptions: Set([subscriptionId]) }], + ]), + }, + (topic) => ({ + ...topic, + clients: topic.clients.update( + client, + { client, subscriptions: Set([subscriptionId]) }, + (client) => ({ + ...client, + subscriptions: client.subscriptions.add(subscriptionId), + }) + ), + }) + ), + }; + } + case 'CLIENT_UNSUBSCRIBED': { + const { topicName, client, subscriptionId } = action.payload; + + return { + ...state, + topics: state.topics.update(topicName, undefined, (topic) => ({ + ...topic, + clients: topic.clients + .update(client, undefined, (topicClient) => ({ + ...topicClient, + subscriptions: topicClient.subscriptions.delete(subscriptionId), + })) + .filter((topicClient) => topicClient.subscriptions.size > 0), + })), + clients: state.clients.update( + client, + undefined, + (broadcasterClient) => ({ + ...broadcasterClient, + subscriptions: + broadcasterClient.subscriptions.delete(subscriptionId), + }) + ), + }; + } + case 'TRANSACTION_RECEIVED': { + const { topicNames, transactionStatus } = action.payload; + + return { + ...state, + topics: topicNames.reduce( + (topics, topicName) => + topics.update( + topicName, + { + topicName, + clients: Map(), + transactions: Map([ + [transactionStatus.signature, transactionStatus], + ]), + }, + (topic) => ({ + ...topic, + transactions: topic.transactions.set( + transactionStatus.signature, + transactionStatus + ), + }) + ), + + state.topics + ), + }; + } + case 'TRANSACTION_CONFIRMED': { + const { topicNames, transactionStatus } = action.payload; + + return { + ...state, + topics: topicNames.reduce( + (topics, topicName) => + topics.update(topicName, undefined, (topic) => ({ + ...topic, + transactions: topic.transactions.update( + transactionStatus.signature, + undefined, + (transaction) => ({ + ...transaction, + ...transactionStatus, + }) + ), + })), + state.topics + ), + }; + } + case 'TRANSACTION_FINALIZED': + case 'TRANSACTION_FAILED': { + const { topicNames, transactionStatus } = action.payload; + + return { + ...state, + topics: topicNames + .reduce( + (topics, topicName) => + topics.update(topicName, undefined, (topic) => ({ + ...topic, + transactions: topic.transactions.delete( + transactionStatus.signature + ), + })), + state.topics + ) + .filter( + (topic) => topic.clients.size > 0 || topic.transactions.size > 0 + ), + }; + } + + default: + return state; + } +}; diff --git a/apps/broadcaster/src/app/events/types.ts b/apps/broadcaster/src/app/events/types.ts new file mode 100644 index 00000000..ffb11e44 --- /dev/null +++ b/apps/broadcaster/src/app/events/types.ts @@ -0,0 +1,108 @@ +import { Finality, Transaction, TransactionSignature } from '@solana/web3.js'; +import { List, Map, Set } from 'immutable'; +import WebSocket from 'ws'; + +export interface TransactionStatus { + signature: TransactionSignature; + status?: Finality; + error?: unknown; + transaction: Transaction; + timestamp: number; +} + +export interface TopicSubscription { + topicName: string; + subscriptionId: string; +} + +export interface Topic { + topicName: string; + clients: Map; + transactions: Map; +} + +export interface BroadcasterClient { + client: WebSocket; + subscriptions: Map; +} + +export interface TopicClient { + client: WebSocket; + subscriptions: Set; +} + +export interface State { + topics: Map; + clients: Map; +} + +export interface ClientConnected { + type: 'CLIENT_CONNECTED'; + payload: WebSocket; +} + +export interface ClientDisconnected { + type: 'CLIENT_DISCONNECTED'; + payload: WebSocket; +} + +export interface ClientSubscribed { + type: 'CLIENT_SUBSCRIBED'; + payload: { + topicName: string; + client: WebSocket; + subscriptionId: string; + correlationId: string; + }; +} + +export interface ClientUnsubscribed { + type: 'CLIENT_UNSUBSCRIBED'; + payload: { + topicName: string; + client: WebSocket; + subscriptionId: string; + }; +} + +export interface TransactionReceived { + type: 'TRANSACTION_RECEIVED'; + payload: { + topicNames: List; + transactionStatus: TransactionStatus; + }; +} + +export interface TransactionConfirmed { + type: 'TRANSACTION_CONFIRMED'; + payload: { + topicNames: List; + transactionStatus: TransactionStatus; + }; +} + +export interface TransactionFinalized { + type: 'TRANSACTION_FINALIZED'; + payload: { + topicNames: List; + transactionStatus: TransactionStatus; + }; +} + +export interface TransactionFailed { + type: 'TRANSACTION_FAILED'; + payload: { + topicNames: List; + transactionStatus: TransactionStatus; + }; +} + +export type ActionTypes = + | ClientConnected + | ClientDisconnected + | ClientSubscribed + | ClientUnsubscribed + | TransactionReceived + | TransactionConfirmed + | TransactionFinalized + | TransactionFailed; diff --git a/apps/broadcaster/src/environments/environment.prod.ts b/apps/broadcaster/src/environments/environment.prod.ts index c9669790..a8c92f68 100644 --- a/apps/broadcaster/src/environments/environment.prod.ts +++ b/apps/broadcaster/src/environments/environment.prod.ts @@ -1,3 +1,4 @@ export const environment = { production: true, + rpcUrl: 'https://api.devnet.solana.com', }; diff --git a/apps/broadcaster/src/environments/environment.ts b/apps/broadcaster/src/environments/environment.ts index a20cfe55..d44952af 100644 --- a/apps/broadcaster/src/environments/environment.ts +++ b/apps/broadcaster/src/environments/environment.ts @@ -1,3 +1,4 @@ export const environment = { production: false, + rpcUrl: 'http://localhost:8899', }; diff --git a/apps/broadcaster/src/main.ts b/apps/broadcaster/src/main.ts index 745b6f1b..e88a7a13 100644 --- a/apps/broadcaster/src/main.ts +++ b/apps/broadcaster/src/main.ts @@ -11,7 +11,7 @@ import { AppModule } from './app/app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useWebSocketAdapter(new WsAdapter(app)); - const port = process.env.PORT || 3333; + const port = process.env.PORT || 4040; await app.listen(port); Logger.log(`🚀 Broadcaster is running on: ws://localhost:${port}`); } diff --git a/apps/bulldozer-client/project.json b/apps/bulldozer-client/project.json index 1244999f..0597632b 100644 --- a/apps/bulldozer-client/project.json +++ b/apps/bulldozer-client/project.json @@ -31,7 +31,8 @@ "scripts": [], "customWebpackConfig": { "path": "apps/bulldozer-client/webpack.config.js" - } + }, + "buildLibsFromSource": false }, "configurations": { "production": { diff --git a/apps/bulldozer-client/src/app/app.module.ts b/apps/bulldozer-client/src/app/app.module.ts index 468f38eb..ee8cf04c 100644 --- a/apps/bulldozer-client/src/app/app.module.ts +++ b/apps/bulldozer-client/src/app/app.module.ts @@ -14,6 +14,7 @@ import { HdSolanaTransactionsInterceptor, } from '@heavy-duty/ngx-solana'; import { HdWalletAdapterModule } from '@heavy-duty/wallet-adapter'; +import { environment } from '../environments/environment'; import { AppComponent } from './app.component'; @NgModule({ @@ -37,13 +38,16 @@ import { AppComponent } from './app.component'; ), }, ], - { initialNavigation: 'enabledBlocking' } + { + initialNavigation: 'enabledNonBlocking', + paramsInheritanceStrategy: 'always', + } ), HdWalletAdapterModule.forRoot({ autoConnect: true, }), HdSolanaModule.forRoot(), - HdBroadcasterModule.forRoot(), + HdBroadcasterModule.forRoot(environment.broadcasterWebsocket), ], bootstrap: [AppComponent], providers: [ diff --git a/apps/bulldozer-client/src/assets/fonts/CaptureIt.ttf b/apps/bulldozer-client/src/assets/fonts/CaptureIt.ttf new file mode 100644 index 00000000..06532298 Binary files /dev/null and b/apps/bulldozer-client/src/assets/fonts/CaptureIt.ttf differ diff --git a/apps/bulldozer-client/src/assets/icons/collection-attribute.svg b/apps/bulldozer-client/src/assets/icons/collection-attribute.svg new file mode 100644 index 00000000..744051e4 --- /dev/null +++ b/apps/bulldozer-client/src/assets/icons/collection-attribute.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/bulldozer-client/src/assets/icons/instruction-argument.svg b/apps/bulldozer-client/src/assets/icons/instruction-argument.svg new file mode 100644 index 00000000..c56469c5 --- /dev/null +++ b/apps/bulldozer-client/src/assets/icons/instruction-argument.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/apps/bulldozer-client/src/assets/icons/instruction-document.svg b/apps/bulldozer-client/src/assets/icons/instruction-document.svg new file mode 100644 index 00000000..88f5845a --- /dev/null +++ b/apps/bulldozer-client/src/assets/icons/instruction-document.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/apps/bulldozer-client/src/assets/icons/instruction-signer.svg b/apps/bulldozer-client/src/assets/icons/instruction-signer.svg new file mode 100644 index 00000000..ee7999cf --- /dev/null +++ b/apps/bulldozer-client/src/assets/icons/instruction-signer.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/bulldozer-client/src/assets/icons/view-profile-icon.png b/apps/bulldozer-client/src/assets/icons/view-profile-icon.png new file mode 100644 index 00000000..f81282dc Binary files /dev/null and b/apps/bulldozer-client/src/assets/icons/view-profile-icon.png differ diff --git a/apps/bulldozer-client/src/assets/images/backgrounds/arguments.png b/apps/bulldozer-client/src/assets/images/backgrounds/arguments.png new file mode 100644 index 00000000..8f60988e Binary files /dev/null and b/apps/bulldozer-client/src/assets/images/backgrounds/arguments.png differ diff --git a/apps/bulldozer-client/src/assets/images/backgrounds/attributes.png b/apps/bulldozer-client/src/assets/images/backgrounds/attributes.png new file mode 100644 index 00000000..0af74a04 Binary files /dev/null and b/apps/bulldozer-client/src/assets/images/backgrounds/attributes.png differ diff --git a/apps/bulldozer-client/src/assets/images/backgrounds/bricks.png b/apps/bulldozer-client/src/assets/images/backgrounds/bricks.png new file mode 100644 index 00000000..8dc55e69 Binary files /dev/null and b/apps/bulldozer-client/src/assets/images/backgrounds/bricks.png differ diff --git a/apps/bulldozer-client/src/assets/images/backgrounds/budget.png b/apps/bulldozer-client/src/assets/images/backgrounds/budget.png new file mode 100644 index 00000000..6d97a068 Binary files /dev/null and b/apps/bulldozer-client/src/assets/images/backgrounds/budget.png differ diff --git a/apps/bulldozer-client/src/assets/images/backgrounds/bulldozer.png b/apps/bulldozer-client/src/assets/images/backgrounds/bulldozer.png new file mode 100644 index 00000000..b8bc7954 Binary files /dev/null and b/apps/bulldozer-client/src/assets/images/backgrounds/bulldozer.png differ diff --git a/apps/bulldozer-client/src/assets/images/backgrounds/button_base.png b/apps/bulldozer-client/src/assets/images/backgrounds/button_base.png new file mode 100644 index 00000000..77e569a3 Binary files /dev/null and b/apps/bulldozer-client/src/assets/images/backgrounds/button_base.png differ diff --git a/apps/bulldozer-client/src/assets/images/backgrounds/code.png b/apps/bulldozer-client/src/assets/images/backgrounds/code.png new file mode 100644 index 00000000..d9968f03 Binary files /dev/null and b/apps/bulldozer-client/src/assets/images/backgrounds/code.png differ diff --git a/apps/bulldozer-client/src/assets/images/backgrounds/documents.png b/apps/bulldozer-client/src/assets/images/backgrounds/documents.png new file mode 100644 index 00000000..e4d97b13 Binary files /dev/null and b/apps/bulldozer-client/src/assets/images/backgrounds/documents.png differ diff --git a/apps/bulldozer-client/src/assets/images/backgrounds/list_element.png b/apps/bulldozer-client/src/assets/images/backgrounds/list_element.png new file mode 100644 index 00000000..5eeb1a04 Binary files /dev/null and b/apps/bulldozer-client/src/assets/images/backgrounds/list_element.png differ diff --git a/apps/bulldozer-client/src/assets/images/backgrounds/sidebar.png b/apps/bulldozer-client/src/assets/images/backgrounds/sidebar.png new file mode 100644 index 00000000..ea4a6429 Binary files /dev/null and b/apps/bulldozer-client/src/assets/images/backgrounds/sidebar.png differ diff --git a/apps/bulldozer-client/src/assets/images/backgrounds/signers.png b/apps/bulldozer-client/src/assets/images/backgrounds/signers.png new file mode 100644 index 00000000..157acdcc Binary files /dev/null and b/apps/bulldozer-client/src/assets/images/backgrounds/signers.png differ diff --git a/apps/bulldozer-client/src/assets/images/backgrounds/tabs.png b/apps/bulldozer-client/src/assets/images/backgrounds/tabs.png new file mode 100644 index 00000000..13d055fe Binary files /dev/null and b/apps/bulldozer-client/src/assets/images/backgrounds/tabs.png differ diff --git a/apps/bulldozer-client/src/assets/images/backgrounds/user_profile.png b/apps/bulldozer-client/src/assets/images/backgrounds/user_profile.png new file mode 100644 index 00000000..320824de Binary files /dev/null and b/apps/bulldozer-client/src/assets/images/backgrounds/user_profile.png differ diff --git a/apps/bulldozer-client/src/assets/images/backgrounds/wood.png b/apps/bulldozer-client/src/assets/images/backgrounds/wood.png new file mode 100644 index 00000000..bfe1d1a8 Binary files /dev/null and b/apps/bulldozer-client/src/assets/images/backgrounds/wood.png differ diff --git a/apps/bulldozer-client/src/assets/images/backgrounds/wood_sign.png b/apps/bulldozer-client/src/assets/images/backgrounds/wood_sign.png new file mode 100644 index 00000000..9e98c472 Binary files /dev/null and b/apps/bulldozer-client/src/assets/images/backgrounds/wood_sign.png differ diff --git a/apps/bulldozer-client/src/assets/images/backgrounds/workspace.png b/apps/bulldozer-client/src/assets/images/backgrounds/workspace.png new file mode 100644 index 00000000..2bf932f6 Binary files /dev/null and b/apps/bulldozer-client/src/assets/images/backgrounds/workspace.png differ diff --git a/apps/bulldozer-client/src/assets/images/default-profile.png b/apps/bulldozer-client/src/assets/images/default-profile.png new file mode 100644 index 00000000..3dc92a88 Binary files /dev/null and b/apps/bulldozer-client/src/assets/images/default-profile.png differ diff --git a/apps/bulldozer-client/src/assets/images/important.png b/apps/bulldozer-client/src/assets/images/important.png new file mode 100644 index 00000000..f566a3fc Binary files /dev/null and b/apps/bulldozer-client/src/assets/images/important.png differ diff --git a/apps/bulldozer-client/src/assets/images/logo.png b/apps/bulldozer-client/src/assets/images/logo.png index fa3080cd..2ed16bb6 100644 Binary files a/apps/bulldozer-client/src/assets/images/logo.png and b/apps/bulldozer-client/src/assets/images/logo.png differ diff --git a/apps/bulldozer-client/src/assets/images/pipe.png b/apps/bulldozer-client/src/assets/images/pipe.png new file mode 100644 index 00000000..c38a874f Binary files /dev/null and b/apps/bulldozer-client/src/assets/images/pipe.png differ diff --git a/apps/bulldozer-client/src/assets/images/social/discord.png b/apps/bulldozer-client/src/assets/images/social/discord.png new file mode 100644 index 00000000..3449b355 Binary files /dev/null and b/apps/bulldozer-client/src/assets/images/social/discord.png differ diff --git a/apps/bulldozer-client/src/assets/images/social/github.png b/apps/bulldozer-client/src/assets/images/social/github.png new file mode 100644 index 00000000..c19484d0 Binary files /dev/null and b/apps/bulldozer-client/src/assets/images/social/github.png differ diff --git a/apps/bulldozer-client/src/assets/images/social/twitter.png b/apps/bulldozer-client/src/assets/images/social/twitter.png new file mode 100644 index 00000000..fd0da305 Binary files /dev/null and b/apps/bulldozer-client/src/assets/images/social/twitter.png differ diff --git a/apps/bulldozer-client/src/assets/images/solana-logo.png b/apps/bulldozer-client/src/assets/images/solana-logo.png new file mode 100644 index 00000000..d6745608 Binary files /dev/null and b/apps/bulldozer-client/src/assets/images/solana-logo.png differ diff --git a/apps/bulldozer-client/src/assets/sounds/button_click.mp3 b/apps/bulldozer-client/src/assets/sounds/button_click.mp3 new file mode 100644 index 00000000..6a72543c Binary files /dev/null and b/apps/bulldozer-client/src/assets/sounds/button_click.mp3 differ diff --git a/apps/bulldozer-client/src/environments/environment.prod.ts b/apps/bulldozer-client/src/environments/environment.prod.ts index 8d35dfa4..da2cb348 100644 --- a/apps/bulldozer-client/src/environments/environment.prod.ts +++ b/apps/bulldozer-client/src/environments/environment.prod.ts @@ -1,6 +1,9 @@ -export const environment = { +import { Environment } from './types'; + +export const environment: Environment = { production: true, - rpcEndpoint: 'http://localhost:8899', - rpcWebsocket: 'ws://localhost:8900', - network: 'localnet', + rpcEndpoint: 'https://api.devnet.solana.com', + rpcWebsocket: 'wss://api.devnet.solana.com', + network: 'devnet', + broadcasterWebsocket: 'wss://broadcaster.heavyduty.builders', }; diff --git a/apps/bulldozer-client/src/environments/environment.ts b/apps/bulldozer-client/src/environments/environment.ts index 8644869f..a090935a 100644 --- a/apps/bulldozer-client/src/environments/environment.ts +++ b/apps/bulldozer-client/src/environments/environment.ts @@ -1,12 +1,13 @@ // This file can be replaced during build by using the `fileReplacements` array. -// `ng build` replaces `environment.ts` with `environment.prod.ts`. -// The list of file replacements can be found in `angular.json`. -export const environment = { +import { Environment } from './types'; + +export const environment: Environment = { production: false, rpcEndpoint: 'http://localhost:8899', rpcWebsocket: 'ws://localhost:8900', - network: 'localnet', + network: 'localhost', + broadcasterWebsocket: 'ws://localhost:4040', }; /* diff --git a/apps/bulldozer-client/src/environments/types.ts b/apps/bulldozer-client/src/environments/types.ts new file mode 100644 index 00000000..aa73908d --- /dev/null +++ b/apps/bulldozer-client/src/environments/types.ts @@ -0,0 +1,10 @@ +import { HttpEndpoint } from '@heavy-duty/ngx-solana'; +import { Network, WebSocketEndpoint } from '@heavy-duty/ngx-websocket'; + +export interface Environment { + production: boolean; + rpcEndpoint: HttpEndpoint; + rpcWebsocket: WebSocketEndpoint; + network: Network; + broadcasterWebsocket: WebSocketEndpoint; +} diff --git a/apps/bulldozer-client/src/index.html b/apps/bulldozer-client/src/index.html index d102e903..b0a99a29 100644 --- a/apps/bulldozer-client/src/index.html +++ b/apps/bulldozer-client/src/index.html @@ -16,7 +16,9 @@ rel="stylesheet" /> - + diff --git a/apps/bulldozer-client/src/styles/base.scss b/apps/bulldozer-client/src/styles/base.scss index e641ef57..2786b25a 100644 --- a/apps/bulldozer-client/src/styles/base.scss +++ b/apps/bulldozer-client/src/styles/base.scss @@ -1,5 +1,7 @@ -@import './constants/colors.scss'; -@import './constants/sizes.scss'; +@import './core/colors.scss'; +@import './core/fonts.scss'; +@import './core/scrollbar.scss'; +@import './core/backgrounds.scss'; // utilities (TO DO: move to antoher folder) @mixin snackbar-span { @@ -65,31 +67,62 @@ input[readonly] { height: inherit !important; } -.bd-custom-top-toolbar { - top: $toolbar-height !important; +.bd-border-bottom { + border-bottom: 2px solid gray; } -.bd-custom-monaco-editor { - height: calc(100vh - 96px) !important; +// Temporal (use theme colors later) + +.hd-side-right { + background-color: #393939; } -.bd-custom-monaco-editor-splitted { - display: block; - height: calc(50vh - 57px) !important; +.hd-side-right { + color: white; } -.bd-border-bottom { - border-bottom: 2px solid gray; +.border-b-1 { + border-bottom-width: 1px; +} + +.bd-box-shadow-bg-red { + box-shadow: 0px 0px 20px 3px red; +} + +.bd-box-shadow-bg-yellow { + box-shadow: 0px 0px 20px 3px yellow; +} + +.bd-box-shadow-bg-green { + box-shadow: 0px 0px 20px 3px green; +} + +.bd-box-shadow-bg-white { + box-shadow: 0px 1px 2px -1px white; +} + +.bd-button { + padding-left: 10px; + padding-right: 10px; + padding-top: 5px; + padding-bottom: 5px; + background-image: linear-gradient(#303030, #262626); + border: 2px solid #212121; +} + +.bd-button:hover { + background-image: linear-gradient(#262626, #262626); } -.bd-custom-height-layout { - height: calc( - 100vh - $toolbar-height - ); // 64 (toolbar size) + 48(tabss toolbar) + 1(extra pixel ?) +.bd-button:active { + background-image: linear-gradient(#262626, #303030); } -.bd-custom-height-content { - height: calc( - 100vh - $toolbar-height - $tabs-height - 1px - ); // 64 (toolbar size) + 48(tabss toolbar) + 1(extra pixel ?) +.bd-button[disabled='disabled'], +.bd-button:disabled { + cursor: not-allowed; + background-image: linear-gradient(gray, gray) !important; + color: #e6e6e6; + border: 1px solid #909090; + opacity: 0.4; } diff --git a/apps/bulldozer-client/src/styles/components/custom-containers.scss b/apps/bulldozer-client/src/styles/components/custom-containers.scss deleted file mode 100644 index 6e19997e..00000000 --- a/apps/bulldozer-client/src/styles/components/custom-containers.scss +++ /dev/null @@ -1,28 +0,0 @@ -@use 'sass:map'; -@use '@angular/material' as mat; -@import '../constants/sizes.scss'; - -// mixings to define color -@mixin color($theme) { - // Get the color config from the theme. - $color-config: mat.get-color-config($theme); - - // Get the primary color palette from the color-config. - $primary-palette: map.get($color-config, 'background'); - - .bd-custom-height-sidebar { - .bd-custom-background { - background-color: mat.get-color-from-palette( - $primary-palette, - 'background' - ); - } - } -} - -@mixin theme($theme) { - $color-config: mat.get-color-config($theme); - @if $color-config != null { - @include color($theme); - } -} diff --git a/apps/bulldozer-client/src/styles/constants/sizes.scss b/apps/bulldozer-client/src/styles/constants/sizes.scss deleted file mode 100644 index 77771fe2..00000000 --- a/apps/bulldozer-client/src/styles/constants/sizes.scss +++ /dev/null @@ -1,3 +0,0 @@ -$actions-buttons-application: 100px; -$toolbar-height: 64px; -$tabs-height: 61px; diff --git a/apps/bulldozer-client/src/styles/core/backgrounds.scss b/apps/bulldozer-client/src/styles/core/backgrounds.scss new file mode 100644 index 00000000..62b21be7 --- /dev/null +++ b/apps/bulldozer-client/src/styles/core/backgrounds.scss @@ -0,0 +1,81 @@ +.bd-bg-image-1, +.bd-bg-image-2, +.bd-bg-image-3, +.bd-bg-image-4, +.bd-bg-image-5, +.bd-bg-image-6, +.bd-bg-image-7, +.bd-bg-image-8, +.bd-bg-image-9, +.bd-bg-image-10, +.bd-bg-image-11, +.bd-bg-image-12, +.bd-bg-image-13, +.bd-bg-image-14, +.bd-bg-image-15 { + background-size: cover; +} + +.bd-bg-image-0 { + background-image: url('../../assets/images/backgrounds/bricks.png'); +} + +.bd-bg-image-1 { + background-image: url('../../assets/images/backgrounds/workspace.png'); +} + +.bd-bg-image-2 { + background-image: url('../../assets/images/backgrounds/budget.png'); +} + +.bd-bg-image-3 { + background-image: url('../../assets/images/backgrounds/attributes.png'); +} + +.bd-bg-image-4 { + background-image: url('../../assets/images/backgrounds/arguments.png'); +} + +.bd-bg-image-5 { + background-image: url('../../assets/images/backgrounds/documents.png'); +} + +.bd-bg-image-6 { + background-image: url('../../assets/images/backgrounds/signers.png'); +} + +.bd-bg-image-7 { + background-image: url('../../assets/images/backgrounds/wood.png'); +} + +.bd-bg-image-8 { + background-image: url('../../assets/images/backgrounds/wood_sign.png'); +} + +.bd-bg-image-9 { + background-image: url('../../assets/images/backgrounds/user_profile.png'); +} + +.bd-bg-image-10 { + background-image: url('../../assets/images/backgrounds/tabs.png'); +} + +.bd-bg-image-11 { + background-image: url('../../assets/images/backgrounds/button_base.png'); +} + +.bd-bg-image-12 { + background-image: url('../../assets/images/backgrounds/sidebar.png'); +} + +.bd-bg-image-13 { + background-image: url('../../assets/images/backgrounds/list_element.png'); +} + +.bd-bg-image-14 { + background-image: url('../../assets/images/backgrounds/code.png'); +} + +.bd-bg-image-15 { + background-image: url('../../assets/images/backgrounds/bulldozer.png'); +} diff --git a/apps/bulldozer-client/src/styles/constants/colors.scss b/apps/bulldozer-client/src/styles/core/colors.scss similarity index 100% rename from apps/bulldozer-client/src/styles/constants/colors.scss rename to apps/bulldozer-client/src/styles/core/colors.scss diff --git a/apps/bulldozer-client/src/styles/core/fonts.scss b/apps/bulldozer-client/src/styles/core/fonts.scss new file mode 100644 index 00000000..27c9818b --- /dev/null +++ b/apps/bulldozer-client/src/styles/core/fonts.scss @@ -0,0 +1,11 @@ +@import 'https://fonts.googleapis.com/css?family=Material+Icons'; +@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400&display=swap'); + +@font-face { + font-family: 'CaptureIt'; + src: url('../../assets/fonts/CaptureIt.ttf'); +} + +.bd-font { + font-family: 'CaptureIt' !important; +} diff --git a/apps/bulldozer-client/src/styles/core/scrollbar.scss b/apps/bulldozer-client/src/styles/core/scrollbar.scss new file mode 100644 index 00000000..aadaf05b --- /dev/null +++ b/apps/bulldozer-client/src/styles/core/scrollbar.scss @@ -0,0 +1,19 @@ +/* width */ +::-webkit-scrollbar { + width: 7px; +} + +/* Track */ +::-webkit-scrollbar-track { + background: rgba(24, 24, 24, 0.4); +} + +/* Handle */ +::-webkit-scrollbar-thumb { + background: #434343; +} + +/* Handle on hover */ +::-webkit-scrollbar-thumb:hover { + background: #343434; +} diff --git a/apps/bulldozer-client/src/styles/material.scss b/apps/bulldozer-client/src/styles/material.scss index 818ac59a..38a07acd 100644 --- a/apps/bulldozer-client/src/styles/material.scss +++ b/apps/bulldozer-client/src/styles/material.scss @@ -1,32 +1,24 @@ @use '@angular/material' as mat; @use '@angular/cdk'; -@use './components/custom-containers.scss' as containers; - -@import './constants/sizes.scss'; -@import './themes/default-themes.scss'; @import '@angular/material/theming'; -@import 'https://fonts.googleapis.com/css?family=Material+Icons'; -@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400&display=swap'); +@import './themes/default-themes.scss'; +@import './core/fonts.scss'; @include mat.core($fontConfig); -$theme: set-light-theme($theme-primary, $theme-accent, $theme-warn); $darkTheme: set-dark-theme($theme-primary, $theme-accent, $theme-warn); -// $bd-background-color: mat.get-color-from-palette($theme); - // Theme Init -@include angular-material-theme($theme); -// set custom themes -@include containers.theme($theme); +@include angular-material-theme($darkTheme); // end -.darkMode { - @include angular-material-theme($darkTheme); - // set custom themes - @include containers.theme($darkTheme); - // end +.hd-border-gray { + border-color: #242424 !important; +} + +.hd-highlight-title { + color: #fba807; } // Specific component overrides, pieces that are not in line with the general theming @@ -134,31 +126,58 @@ mat-icon { max-width: unset !important; } -.mat-fab.cdk-keyboard-focused, -.mat-mini-fab.cdk-keyboard-focused { - transform: scale(1.5); - box-shadow: 0px 3px 5px -1px rgb(0 0 0 / 20%), - 0px 6px 10px 0px rgb(0 0 0 / 14%), 0px 1px 18px 0px rgb(0 0 0 / 12%) !important; -} - -.mat-tab-links { - height: $tabs-height; -} - .mat-tab-link { - height: $tabs-height !important; + height: auto !important; } .darkMode { - .mat-fab.cdk-keyboard-focused, - .mat-mini-fab.cdk-keyboard-focused { - box-shadow: 0px 3px 5px -1px rgb(255 255 255 / 20%), - 0px 6px 10px 0px rgb(255 255 255 / 14%), - 0px 1px 18px 0px rgb(255 255 255 / 12%) !important; - } - .bd-custom-color .mat-accent.mat-button-disabled { color: #444444 !important; background-color: rgba(250, 250, 250, 0.42) !important; } } + +.mat-drawer-container { + height: 100vh; +} + +.mat-dialog-container { + background: transparent !important; +} + +.mat-tab-header { + border-color: transparent !important; +} + +.mat-drawer-backdrop.mat-drawer-shown { + background-color: rgba(30, 30, 30, 0.7); +} + +.mat-snack-bar-container { + display: flex !important; + justify-content: center !important; + margin: 0 !important; + padding: 0 !important; + background-color: transparent !important; + box-shadow: none !important; +} + +.mat-tab-link-container { + padding: 5px; +} + +.mat-tab-link-container .mat-tab-link { + height: 40px !important; +} + +.mat-tab-link-container .mat-ink-bar { + background-color: white !important; + height: 1px !important; + opacity: 0.3 !important; + box-shadow: 0px 0px 5px 1px white; +} + +.mat-expansion-panel { + background-color: transparent !important; + box-shadow: none !important; +} diff --git a/apps/bulldozer-client/src/styles/themes/default-themes.scss b/apps/bulldozer-client/src/styles/themes/default-themes.scss index 156df45c..7b09bcbe 100644 --- a/apps/bulldozer-client/src/styles/themes/default-themes.scss +++ b/apps/bulldozer-client/src/styles/themes/default-themes.scss @@ -88,9 +88,9 @@ $light-bg-darker-10: darken($light-background, 10%); $light-bg-darker-20: darken($light-background, 20%); $light-bg-darker-30: darken($light-background, 30%); $light-bg-lighter-5: lighten($light-background, 5%); -$dark-bg-tooltip: lighten(#2c2c2c, 20%); -$dark-bg-alpha-4: rgba(#2c2c2c, 0.04); -$dark-bg-alpha-12: rgba(#2c2c2c, 0.12); +$dark-bg-tooltip: lighten(#242424, 20%); +$dark-bg-alpha-4: rgba(#242424, 0.04); +$dark-bg-alpha-12: rgba(#242424, 0.12); $mat-light-theme-background: ( background: $light-background, @@ -111,7 +111,7 @@ $mat-light-theme-background: ( ); // Dark bg -$dark-background: #2c2c2c; +$dark-background: rgba(30, 30, 30, 1); $dark-bg-lighter-5: lighten($dark-background, 5%); $dark-bg-lighter-10: lighten($dark-background, 10%); $dark-bg-lighter-20: lighten($dark-background, 20%); diff --git a/apps/bulldozer-programs/Cargo.lock b/apps/bulldozer-programs/Cargo.lock index 1aaca62f..4b2cd25b 100644 --- a/apps/bulldozer-programs/Cargo.lock +++ b/apps/bulldozer-programs/Cargo.lock @@ -19,9 +19,9 @@ dependencies = [ [[package]] name = "anchor-attribute-access-control" -version = "0.22.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb45cc9d1ce72e5eda341126de495a2c3810108c2333c6f3b4e09d99605f3f48" +checksum = "a9b75d05b6b4ac9d95bb6e3b786b27d3a708c4c5a87c92ffaa25bbe9ae4c5d91" dependencies = [ "anchor-syn", "anyhow", @@ -33,9 +33,9 @@ dependencies = [ [[package]] name = "anchor-attribute-account" -version = "0.22.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16406bd1c27ff4ebdca4f5d5b09b7952f4d161f25094243e09355797c6bddaa6" +checksum = "485351a6d8157750d10d88c8e256f1bf8339262b2220ae9125aed3471309b5de" dependencies = [ "anchor-syn", "anyhow", @@ -48,9 +48,9 @@ dependencies = [ [[package]] name = "anchor-attribute-constant" -version = "0.22.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d347ce462ceba4473d216bab2c9d0d9702a027d25e93b5376d8d8593d9e13de0" +checksum = "dc632c540913dd051a78b00587cc47f57013d303163ddfaf4fa18717f7ccc1e0" dependencies = [ "anchor-syn", "proc-macro2", @@ -59,9 +59,9 @@ dependencies = [ [[package]] name = "anchor-attribute-error" -version = "0.22.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "354582d796f8309252d18f787f0e49df8ab6fdfe48f838f059f001ee2f04b5c8" +checksum = "3b5bd1dcfa7f3bc22dacef233d70a9e0bee269c4ac484510662f257cba2353a1" dependencies = [ "anchor-syn", "proc-macro2", @@ -71,9 +71,9 @@ dependencies = [ [[package]] name = "anchor-attribute-event" -version = "0.22.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2e218dd8a446993463e38c00159349ae25aa76076191cde0ba460c9c65a180" +checksum = "6c6f9e6ce551ac9a177a45c99a65699a860c9e95fac68675138af1246e2591b0" dependencies = [ "anchor-syn", "anyhow", @@ -84,9 +84,9 @@ dependencies = [ [[package]] name = "anchor-attribute-interface" -version = "0.22.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e1e536e15b13e3168cf878a90b1bd2dfff1b4c8c9475be4b87f71b20cf8e85d" +checksum = "d104aa17418cb329ed7418b227e083d5f326a27f26ce98f5d92e33da62a5f459" dependencies = [ "anchor-syn", "anyhow", @@ -98,9 +98,9 @@ dependencies = [ [[package]] name = "anchor-attribute-program" -version = "0.22.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6519b3ac626c1bd9df407fe22ec6a283f4b1067ee7f3be896ca580be510b7196" +checksum = "b6831b920b173c004ddf7ae1167d1d25e9f002ffcb1773bbc5c7ce532a4441e1" dependencies = [ "anchor-syn", "anyhow", @@ -111,9 +111,9 @@ dependencies = [ [[package]] name = "anchor-attribute-state" -version = "0.22.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88e6a21070bcb053f092a1a9054924e8a1b5afd68f7317d0138327401ac154e1" +checksum = "cde147b10c71d95dc679785db0b5f3abac0091f789167aa62ac0135e2f54e8b9" dependencies = [ "anchor-syn", "anyhow", @@ -124,9 +124,9 @@ dependencies = [ [[package]] name = "anchor-derive-accounts" -version = "0.22.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a65890c2132f30a3ff160fb83f74e0a0454f904f46f1c9be38d3e94c2d06ed" +checksum = "9cde98a0e1a56046b040ff591dfda391f88917af2b6487d02b45093c05be3514" dependencies = [ "anchor-syn", "anyhow", @@ -137,9 +137,9 @@ dependencies = [ [[package]] name = "anchor-lang" -version = "0.22.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef066f4bc0cb4080ff6244b6a66ef31b6077e0302738b365ca894540f5b7dcf8" +checksum = "a85dd2c5e29e20c7f4701a43724d6cd5406d0ee5694705522e43da0f26542a84" dependencies = [ "anchor-attribute-access-control", "anchor-attribute-account", @@ -161,9 +161,9 @@ dependencies = [ [[package]] name = "anchor-syn" -version = "0.22.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "506cb44e4e895f917566c7a0554e487a001041d82dd3ae9f1f37ae7f20f86222" +checksum = "03549dc2eae0b20beba6333b14520e511822a6321cdb1760f841064a69347316" dependencies = [ "anyhow", "bs58 0.3.1", @@ -977,9 +977,9 @@ checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" [[package]] name = "solana-frozen-abi" -version = "1.9.1" +version = "1.9.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c18abe9d7b525215f3de16db3fca6057affdd73464ca027786b42455257b9" +checksum = "c8db0d37f7c345c6417898e675d218d76a1ce6d3bd57584d7f463d48badf1541" dependencies = [ "bs58 0.4.0", "bv", @@ -997,9 +997,9 @@ dependencies = [ [[package]] name = "solana-frozen-abi-macro" -version = "1.9.1" +version = "1.9.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4221e1b9cfad821fa097a61c1827f8214c2ef97057c270d5f68a52d90929ae2a" +checksum = "023560984c7f16a53e280866c177d1ad45225614356224c1ade671de16424466" dependencies = [ "proc-macro2", "quote", @@ -1009,9 +1009,9 @@ dependencies = [ [[package]] name = "solana-logger" -version = "1.9.1" +version = "1.9.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a5b4d8fe05eab74ca3c7afd79e6143bed4028997bf36de06b7f503b55b125c" +checksum = "57cb0a4ef4dd740397addf5fa50d9dff572371fd47df2bdecc5fb530546490e2" dependencies = [ "env_logger", "lazy_static", @@ -1020,9 +1020,9 @@ dependencies = [ [[package]] name = "solana-program" -version = "1.9.1" +version = "1.9.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d577959f537ba4435db97588fe2ea5520c98483ef5fcf6a0fb8ca096f82213" +checksum = "9654224bf5d4c6d80f68c3c996683b389693af1c69103af667c683180bad6c5e" dependencies = [ "base64 0.13.0", "bincode", @@ -1063,9 +1063,9 @@ dependencies = [ [[package]] name = "solana-sdk-macro" -version = "1.9.1" +version = "1.9.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0664789b09d768bfab1fc34c532d8cd1abfc63b2ec8174c7b65e6b17f08e7841" +checksum = "bac8cb60eb2e4c85d76ea1f0429dfc0e8b4ba7834e9d69695bb3164f3966e16d" dependencies = [ "bs58 0.4.0", "proc-macro2", diff --git a/apps/bulldozer-programs/programs/bulldozer/Cargo.toml b/apps/bulldozer-programs/programs/bulldozer/Cargo.toml index 09c98c9c..cc2b05dc 100644 --- a/apps/bulldozer-programs/programs/bulldozer/Cargo.toml +++ b/apps/bulldozer-programs/programs/bulldozer/Cargo.toml @@ -15,4 +15,4 @@ cpi = ["no-entrypoint"] default = [] [dependencies] -anchor-lang = "0.22.1" +anchor-lang = "0.24.2" diff --git a/apps/bulldozer-programs/programs/bulldozer/src/collections/budget.rs b/apps/bulldozer-programs/programs/bulldozer/src/collections/budget.rs index e792528d..741026b0 100644 --- a/apps/bulldozer-programs/programs/bulldozer/src/collections/budget.rs +++ b/apps/bulldozer-programs/programs/bulldozer/src/collections/budget.rs @@ -5,6 +5,8 @@ pub struct Budget { pub authority: Pubkey, pub workspace: Pubkey, pub bump: u8, + pub total_deposited: u64, + pub total_value_locked: u64, pub created_at: i64, } @@ -12,6 +14,8 @@ impl Budget { pub fn initialize(&mut self, authority: Pubkey, workspace: Pubkey, bump: u8) -> () { self.workspace = workspace; self.authority = authority; + self.total_deposited = 0; + self.total_value_locked = 0; self.bump = bump; } @@ -21,8 +25,18 @@ impl Budget { } pub fn space() -> usize { - // discriminator + authority + workspace + bump + created at - 8 + 32 + 32 + 1 + 8 + // discriminator + authority + workspace + bump + // total deposited + total value locked + created at + 8 + 32 + 32 + 1 + 8 + 8 + 8 + } + + pub fn deposit(&mut self, amount: u64) -> () { + self.total_deposited = self.total_deposited.checked_add(amount).unwrap(); + self.total_value_locked = self.total_value_locked.checked_add(amount).unwrap(); + } + + pub fn withdraw(&mut self, amount: u64) -> () { + self.total_value_locked = self.total_value_locked.checked_sub(amount).unwrap(); } pub fn get_rent_exemption() -> Result { diff --git a/apps/bulldozer-programs/programs/bulldozer/src/collections/instruction_account.rs b/apps/bulldozer-programs/programs/bulldozer/src/collections/instruction_account.rs index bc8edca6..fa61761b 100644 --- a/apps/bulldozer-programs/programs/bulldozer/src/collections/instruction_account.rs +++ b/apps/bulldozer-programs/programs/bulldozer/src/collections/instruction_account.rs @@ -25,6 +25,62 @@ impl InstructionAccountStats { } } +#[account] +pub struct InstructionAccountCollection { + pub collection: Option, +} + +impl InstructionAccountCollection { + pub fn set(&mut self, collection: Option) -> () { + self.collection = collection; + } + + pub fn space() -> usize { + // discriminator + collection public key + 8 + 33 + } +} + +#[account] +pub struct InstructionAccountPayer { + pub payer: Option, +} + +impl InstructionAccountPayer { + pub fn set(&mut self, payer: Option) -> () { + self.payer = payer; + } + + pub fn space() -> usize { + // discriminator + collection public key + 8 + 33 + } +} + +#[account] +pub struct InstructionAccountClose { + pub close: Option, +} + +impl InstructionAccountClose { + pub fn set(&mut self, close: Option) -> () { + self.close = close; + } + + pub fn space() -> usize { + // discriminator + collection public key + 8 + 33 + } +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct InstructionAccountBumps { + pub stats: u8, + pub collection: u8, + pub payer: u8, + pub close: u8, +} + #[account] pub struct InstructionAccount { pub authority: Pubkey, @@ -34,13 +90,10 @@ pub struct InstructionAccount { pub name: String, pub kind: AccountKinds, pub modifier: Option, - pub collection: Option, - pub payer: Option, - pub close: Option, pub space: Option, pub created_at: i64, pub updated_at: i64, - pub instruction_account_stats_bump: u8, + pub bumps: InstructionAccountBumps, } impl InstructionAccount { @@ -53,7 +106,8 @@ impl InstructionAccount { instruction: Pubkey, kind: AccountKinds, modifier: Option, - instruction_account_stats_bump: u8, + space: Option, + bumps: InstructionAccountBumps, ) -> () { self.authority = authority; self.workspace = workspace; @@ -62,18 +116,22 @@ impl InstructionAccount { self.name = name; self.kind = kind; self.modifier = modifier; - self.instruction_account_stats_bump = instruction_account_stats_bump; + self.space = space; + self.bumps = bumps; } pub fn rename(&mut self, name: String) -> () { self.name = name; } - pub fn change_settings(&mut self, kind: AccountKinds, modifier: Option) -> () { - self.kind = kind; + pub fn set_modifier(&mut self, modifier: Option) -> () { self.modifier = modifier; } + pub fn set_space(&mut self, space: Option) -> () { + self.space = space; + } + pub fn initialize_timestamp(&mut self) -> Result<()> { self.created_at = Clock::get()?.unix_timestamp; self.updated_at = Clock::get()?.unix_timestamp; @@ -88,8 +146,8 @@ impl InstructionAccount { pub fn space() -> usize { // discriminator + authority + workspace + application // instruction + name (size 32 + 4 ?) + kind + modifier - // collection + payer + close + space + stats bump - // created at + updated at - 8 + 32 + 32 + 32 + 32 + 36 + 2 + 2 + 33 + 33 + 33 + 3 + 1 + 8 + 8 + // payer + close + space + stats bump + collection bump + // payer bump + created at + updated at + 8 + 32 + 32 + 32 + 32 + 36 + 2 + 2 + 33 + 33 + 3 + 1 + 1 + 1 + 8 + 8 } } diff --git a/apps/bulldozer-programs/programs/bulldozer/src/collections/user.rs b/apps/bulldozer-programs/programs/bulldozer/src/collections/user.rs index 06961a5c..7594ce60 100644 --- a/apps/bulldozer-programs/programs/bulldozer/src/collections/user.rs +++ b/apps/bulldozer-programs/programs/bulldozer/src/collections/user.rs @@ -3,23 +3,51 @@ use anchor_lang::prelude::*; #[account] pub struct User { pub authority: Pubkey, + pub user_name: String, + pub name: String, + pub thumbnail_url: String, pub bump: u8, pub created_at: i64, + pub updated_at: i64, } impl User { - pub fn initialize(&mut self, authority: Pubkey, bump: u8) -> () { + pub fn initialize( + &mut self, + authority: Pubkey, + user_name: String, + name: String, + thumbnail_url: String, + bump: u8, + ) -> () { self.authority = authority; + self.user_name = user_name; + self.name = name; + self.thumbnail_url = thumbnail_url; self.bump = bump; } + pub fn update(&mut self, user_name: String, name: String, thumbnail_url: String) -> () { + self.user_name = user_name; + self.name = name; + self.thumbnail_url = thumbnail_url; + } + pub fn initialize_timestamp(&mut self) -> Result<()> { self.created_at = Clock::get()?.unix_timestamp; + self.updated_at = Clock::get()?.unix_timestamp; + Ok(()) + } + + pub fn bump_timestamp(&mut self) -> Result<()> { + self.updated_at = Clock::get()?.unix_timestamp; Ok(()) } pub fn space() -> usize { - // discriminator + authority + bump + created at - 8 + 32 + 1 + 8 + // discriminator + authority + username + // name + thumbnail + bump + // created at + updated at + 8 + 32 + 19 + 36 + 104 + 1 + 8 + 8 } } diff --git a/apps/bulldozer-programs/programs/bulldozer/src/enums/account_kinds.rs b/apps/bulldozer-programs/programs/bulldozer/src/enums/account_kinds.rs index c9ad52cb..1785ccf7 100644 --- a/apps/bulldozer-programs/programs/bulldozer/src/enums/account_kinds.rs +++ b/apps/bulldozer-programs/programs/bulldozer/src/enums/account_kinds.rs @@ -3,18 +3,15 @@ use anchor_lang::prelude::*; #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq)] pub enum AccountKinds { - Document { id: u8, collection: Pubkey }, + Document { id: u8 }, Signer { id: u8 }, } impl AccountKinds { - pub fn create(kind: u8, collection: Option) -> Result { - match (kind, collection) { - (0, Some(collection)) => Ok(AccountKinds::Document { - id: 0, - collection: collection, - }), - (1, _) => Ok(AccountKinds::Signer { id: 1 }), + pub fn create(kind: u8) -> Result { + match kind { + 0 => Ok(AccountKinds::Document { id: 0 }), + 1 => Ok(AccountKinds::Signer { id: 1 }), _ => Err(error!(ErrorCode::InvalidAccountKind)), } } diff --git a/apps/bulldozer-programs/programs/bulldozer/src/enums/account_modifiers.rs b/apps/bulldozer-programs/programs/bulldozer/src/enums/account_modifiers.rs index 4736a50d..50383e63 100644 --- a/apps/bulldozer-programs/programs/bulldozer/src/enums/account_modifiers.rs +++ b/apps/bulldozer-programs/programs/bulldozer/src/enums/account_modifiers.rs @@ -3,35 +3,16 @@ use anchor_lang::prelude::*; #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq)] pub enum AccountModifiers { - Init { - id: u8, - space: Option, - payer: Option, - }, - Mut { - id: u8, - close: Option, - }, + Init { id: u8 }, + Mut { id: u8 }, } impl AccountModifiers { - pub fn create( - modifier: Option, - space: Option, - payer: Option, - close: Option, - ) -> Result> { - match (modifier, space, payer, close) { - (Some(0), space, payer, _) => Ok(Some(AccountModifiers::Init { - id: 0, - space: space, - payer: payer, - })), - (Some(1), _, _, close) => Ok(Some(AccountModifiers::Mut { - id: 1, - close: close, - })), - (None, _, _, _) => Ok(None), + pub fn create(modifier: Option) -> Result> { + match modifier { + Some(0) => Ok(Some(AccountModifiers::Init { id: 0 })), + Some(1) => Ok(Some(AccountModifiers::Mut { id: 1 })), + None => Ok(None), _ => Err(error!(ErrorCode::InvalidAccountModifier)), } } diff --git a/apps/bulldozer-programs/programs/bulldozer/src/errors.rs b/apps/bulldozer-programs/programs/bulldozer/src/errors.rs index 4509e0aa..ee1d8e48 100644 --- a/apps/bulldozer-programs/programs/bulldozer/src/errors.rs +++ b/apps/bulldozer-programs/programs/bulldozer/src/errors.rs @@ -94,4 +94,14 @@ pub enum ErrorCode { InstructionAccountDoesNotBelongToInstruction, #[msg("Only admin collaborator can update")] OnlyAdminCollaboratorCanUpdate, + #[msg("Only document accounts can have collection")] + OnlyDocumentAccountsCanHaveCollection, + #[msg("Only init accounts can have a payer")] + OnlyInitAccountsCanHavePayer, + #[msg("Only mut accounts can have a close")] + OnlyMutAccountsCanHaveClose, + #[msg("Arithmetic Error")] + ArithmeticError, + #[msg("Unauthorized Withdraw")] + UnauthorizedWithdraw, } diff --git a/apps/bulldozer-programs/programs/bulldozer/src/instructions/application/create_application.rs b/apps/bulldozer-programs/programs/bulldozer/src/instructions/application/create_application.rs index 07dbc9e3..d5b903e0 100644 --- a/apps/bulldozer-programs/programs/bulldozer/src/instructions/application/create_application.rs +++ b/apps/bulldozer-programs/programs/bulldozer/src/instructions/application/create_application.rs @@ -3,7 +3,7 @@ use crate::collections::{ }; use crate::enums::CollaboratorStatus; use crate::errors::ErrorCode; -use crate::utils::fund_rent_for_account; +use crate::utils::transfer_lamports; use anchor_lang::prelude::*; #[derive(AnchorSerialize, AnchorDeserialize)] @@ -100,12 +100,12 @@ pub fn handle( arguments: CreateApplicationArguments, ) -> Result<()> { msg!("Create application"); - fund_rent_for_account( + transfer_lamports( ctx.accounts.budget.to_account_info(), ctx.accounts.authority.to_account_info(), **ctx.accounts.application.to_account_info().lamports.borrow(), )?; - fund_rent_for_account( + transfer_lamports( ctx.accounts.budget.to_account_info(), ctx.accounts.authority.to_account_info(), **ctx diff --git a/apps/bulldozer-programs/programs/bulldozer/src/instructions/collection/create_collection.rs b/apps/bulldozer-programs/programs/bulldozer/src/instructions/collection/create_collection.rs index 82da0ceb..c9918508 100644 --- a/apps/bulldozer-programs/programs/bulldozer/src/instructions/collection/create_collection.rs +++ b/apps/bulldozer-programs/programs/bulldozer/src/instructions/collection/create_collection.rs @@ -3,7 +3,7 @@ use crate::collections::{ }; use crate::enums::CollaboratorStatus; use crate::errors::ErrorCode; -use crate::utils::fund_rent_for_account; +use crate::utils::transfer_lamports; use anchor_lang::prelude::*; #[derive(AnchorSerialize, AnchorDeserialize)] @@ -99,12 +99,12 @@ pub fn validate(ctx: &Context) -> Result { pub fn handle(ctx: Context, arguments: CreateCollectionArguments) -> Result<()> { msg!("Create collection"); - fund_rent_for_account( + transfer_lamports( ctx.accounts.budget.to_account_info(), ctx.accounts.authority.to_account_info(), **ctx.accounts.collection.to_account_info().lamports.borrow(), )?; - fund_rent_for_account( + transfer_lamports( ctx.accounts.budget.to_account_info(), ctx.accounts.authority.to_account_info(), **ctx diff --git a/apps/bulldozer-programs/programs/bulldozer/src/instructions/collection_attribute/create_collection_attribute.rs b/apps/bulldozer-programs/programs/bulldozer/src/instructions/collection_attribute/create_collection_attribute.rs index f1c85c68..4dc9c615 100644 --- a/apps/bulldozer-programs/programs/bulldozer/src/instructions/collection_attribute/create_collection_attribute.rs +++ b/apps/bulldozer-programs/programs/bulldozer/src/instructions/collection_attribute/create_collection_attribute.rs @@ -4,7 +4,7 @@ use crate::collections::{ }; use crate::enums::{AttributeKinds, AttributeModifiers, CollaboratorStatus}; use crate::errors::ErrorCode; -use crate::utils::fund_rent_for_account; +use crate::utils::transfer_lamports; use anchor_lang::prelude::*; #[derive(AnchorSerialize, AnchorDeserialize)] @@ -104,7 +104,7 @@ pub fn handle( arguments: CreateCollectionAttributeArguments, ) -> Result<()> { msg!("Create collection attribute"); - fund_rent_for_account( + transfer_lamports( ctx.accounts.budget.to_account_info(), ctx.accounts.authority.to_account_info(), **ctx.accounts.attribute.to_account_info().lamports.borrow(), diff --git a/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction/create_instruction.rs b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction/create_instruction.rs index 451b9058..8e181552 100644 --- a/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction/create_instruction.rs +++ b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction/create_instruction.rs @@ -4,7 +4,7 @@ use crate::collections::{ }; use crate::enums::CollaboratorStatus; use crate::errors::ErrorCode; -use crate::utils::fund_rent_for_account; +use crate::utils::transfer_lamports; use anchor_lang::prelude::*; #[derive(AnchorSerialize, AnchorDeserialize)] @@ -106,12 +106,12 @@ pub fn handle( arguments: CreateInstructionArguments, ) -> Result<()> { msg!("Create instruction"); - fund_rent_for_account( + transfer_lamports( ctx.accounts.budget.to_account_info(), ctx.accounts.authority.to_account_info(), **ctx.accounts.instruction.to_account_info().lamports.borrow(), )?; - fund_rent_for_account( + transfer_lamports( ctx.accounts.budget.to_account_info(), ctx.accounts.authority.to_account_info(), **ctx diff --git a/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/clear_instruction_account_close.rs b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/clear_instruction_account_close.rs new file mode 100644 index 00000000..7844d668 --- /dev/null +++ b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/clear_instruction_account_close.rs @@ -0,0 +1,54 @@ +use crate::collections::{ + Collaborator, Instruction, InstructionAccount, InstructionAccountClose, User, Workspace, +}; +use crate::enums::CollaboratorStatus; +use crate::errors::ErrorCode; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct ClearInstructionAccountClose<'info> { + pub authority: Signer<'info>, + pub workspace: Box>, + #[account( + constraint = instruction.workspace == workspace.key() @ ErrorCode::InstructionDoesNotBelongToWorkspace, + )] + pub instruction: Box>, + #[account( + constraint = account.workspace == workspace.key() @ ErrorCode::InstructionAccountDoesNotBelongToWorkspace, + constraint = account.instruction == instruction.key() @ ErrorCode::InstructionAccountDoesNotBelongToInstruction, + )] + pub account: Box>, + #[account( + mut, + seeds = [ + b"instruction_account_close".as_ref(), + account.key().as_ref(), + ], + bump = account.bumps.close + )] + pub account_close: Box>, + #[account( + seeds = [ + b"user".as_ref(), + authority.key().as_ref(), + ], + bump = user.bump + )] + pub user: Box>, + #[account( + seeds = [ + b"collaborator".as_ref(), + workspace.key().as_ref(), + user.key().as_ref(), + ], + bump = collaborator.bump, + constraint = collaborator.status == CollaboratorStatus::Approved { id: 1 } @ ErrorCode::CollaboratorStatusNotApproved, + )] + pub collaborator: Box>, +} + +pub fn handle(ctx: Context) -> Result<()> { + msg!("Clear instruction account close"); + ctx.accounts.account_close.set(None); + Ok(()) +} diff --git a/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/create_instruction_account.rs b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/create_instruction_account.rs index 2450c3f7..12ac076b 100644 --- a/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/create_instruction_account.rs +++ b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/create_instruction_account.rs @@ -1,10 +1,11 @@ use crate::collections::{ - Application, Budget, Collaborator, Collection, Instruction, InstructionAccount, + Application, Budget, Collaborator, Instruction, InstructionAccount, InstructionAccountBumps, + InstructionAccountClose, InstructionAccountCollection, InstructionAccountPayer, InstructionAccountStats, InstructionStats, User, Workspace, }; use crate::enums::{AccountKinds, AccountModifiers, CollaboratorStatus}; use crate::errors::ErrorCode; -use crate::utils::{fund_rent_for_account, get_account_key, get_remaining_account}; +use crate::utils::transfer_lamports; use anchor_lang::prelude::*; #[derive(AnchorSerialize, AnchorDeserialize)] @@ -82,22 +83,47 @@ pub struct CreateInstructionAccount<'info> { bump )] pub account_stats: Box>, + #[account( + init, + payer = authority, + space = InstructionAccountCollection::space(), + seeds = [ + b"instruction_account_collection".as_ref(), + account.key().as_ref(), + ], + bump + )] + pub account_collection: Box>, + #[account( + init, + payer = authority, + space = InstructionAccountPayer::space(), + seeds = [ + b"instruction_account_payer".as_ref(), + account.key().as_ref(), + ], + bump + )] + pub account_payer: Box>, + #[account( + init, + payer = authority, + space = InstructionAccountClose::space(), + seeds = [ + b"instruction_account_close".as_ref(), + account.key().as_ref(), + ], + bump + )] + pub account_close: Box>, } pub fn validate( ctx: &Context, arguments: &CreateInstructionAccountArguments, ) -> Result { - match ( - arguments.kind, - get_remaining_account::(ctx.remaining_accounts, 0)?, - arguments.modifier, - arguments.space, - get_remaining_account::(ctx.remaining_accounts, 1)?, - ) { - (0, None, _, _, _) => Err(error!(ErrorCode::MissingCollectionAccount)), - (_, _, Some(0), None, _) => Err(error!(ErrorCode::MissingSpace)), - (_, _, Some(0), _, None) => Err(error!(ErrorCode::MissingPayerAccount)), + match (arguments.modifier, arguments.space) { + (Some(0), None) => Err(error!(ErrorCode::MissingSpace)), _ => { let budget_lamports = **ctx.accounts.budget.to_account_info().lamports.borrow(); let instruction_account_rent = **ctx.accounts.account.to_account_info().lamports.borrow(); @@ -107,10 +133,34 @@ pub fn validate( .to_account_info() .lamports .borrow(); + let instruction_account_collection_rent = **ctx + .accounts + .account_collection + .to_account_info() + .lamports + .borrow(); + let instruction_account_payer_rent = **ctx + .accounts + .account_payer + .to_account_info() + .lamports + .borrow(); + let instruction_account_close_rent = **ctx + .accounts + .account_close + .to_account_info() + .lamports + .borrow(); let funds_required = &Budget::get_rent_exemption()? .checked_add(instruction_account_rent) .unwrap() .checked_add(instruction_account_stats_rent) + .unwrap() + .checked_add(instruction_account_collection_rent) + .unwrap() + .checked_add(instruction_account_payer_rent) + .unwrap() + .checked_add(instruction_account_close_rent) .unwrap(); if budget_lamports.lt(funds_required) { @@ -127,12 +177,12 @@ pub fn handle( arguments: CreateInstructionAccountArguments, ) -> Result<()> { msg!("Create instruction account"); - fund_rent_for_account( + transfer_lamports( ctx.accounts.budget.to_account_info(), ctx.accounts.authority.to_account_info(), **ctx.accounts.account.to_account_info().lamports.borrow(), )?; - fund_rent_for_account( + transfer_lamports( ctx.accounts.budget.to_account_info(), ctx.accounts.authority.to_account_info(), **ctx @@ -142,35 +192,57 @@ pub fn handle( .lamports .borrow(), )?; + transfer_lamports( + ctx.accounts.budget.to_account_info(), + ctx.accounts.authority.to_account_info(), + **ctx + .accounts + .account_collection + .to_account_info() + .lamports + .borrow(), + )?; + transfer_lamports( + ctx.accounts.budget.to_account_info(), + ctx.accounts.authority.to_account_info(), + **ctx + .accounts + .account_payer + .to_account_info() + .lamports + .borrow(), + )?; + transfer_lamports( + ctx.accounts.budget.to_account_info(), + ctx.accounts.authority.to_account_info(), + **ctx + .accounts + .account_close + .to_account_info() + .lamports + .borrow(), + )?; ctx.accounts.account.initialize( arguments.name, ctx.accounts.authority.key(), ctx.accounts.workspace.key(), ctx.accounts.application.key(), ctx.accounts.instruction.key(), - AccountKinds::create( - arguments.kind, - get_account_key(get_remaining_account::( - ctx.remaining_accounts, - 0, - )?)?, - )?, - AccountModifiers::create( - arguments.modifier, - arguments.space, - get_account_key(get_remaining_account::( - ctx.remaining_accounts, - 1, - )?)?, - get_account_key(get_remaining_account::( - ctx.remaining_accounts, - 1, - )?)?, - )?, - *ctx.bumps.get("account_stats").unwrap(), + AccountKinds::create(arguments.kind)?, + AccountModifiers::create(arguments.modifier)?, + arguments.space, + InstructionAccountBumps { + stats: *ctx.bumps.get("account_stats").unwrap(), + collection: *ctx.bumps.get("account_collection").unwrap(), + payer: *ctx.bumps.get("account_payer").unwrap(), + close: *ctx.bumps.get("account_close").unwrap(), + }, ); ctx.accounts.account.initialize_timestamp()?; ctx.accounts.account_stats.initialize(); + ctx.accounts.account_collection.set(None); + ctx.accounts.account_payer.set(None); + ctx.accounts.account_close.set(None); ctx.accounts.instruction_stats.increase_account_quantity(); Ok(()) } diff --git a/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/delete_instruction_account.rs b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/delete_instruction_account.rs index ae504d1c..077a9b8c 100644 --- a/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/delete_instruction_account.rs +++ b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/delete_instruction_account.rs @@ -1,5 +1,6 @@ use crate::collections::{ - Budget, Collaborator, Instruction, InstructionAccount, InstructionAccountStats, InstructionStats, + Budget, Collaborator, Instruction, InstructionAccount, InstructionAccountClose, + InstructionAccountCollection, InstructionAccountPayer, InstructionAccountStats, InstructionStats, User, Workspace, }; use crate::enums::CollaboratorStatus; @@ -65,9 +66,39 @@ pub struct DeleteInstructionAccount<'info> { b"instruction_account_stats".as_ref(), account.key().as_ref() ], - bump = account.instruction_account_stats_bump + bump = account.bumps.stats )] pub account_stats: Box>, + #[account( + mut, + close = budget, + seeds = [ + b"instruction_account_collection".as_ref(), + account.key().as_ref() + ], + bump = account.bumps.collection + )] + pub account_collection: Box>, + #[account( + mut, + close = budget, + seeds = [ + b"instruction_account_close".as_ref(), + account.key().as_ref() + ], + bump = account.bumps.close + )] + pub account_close: Box>, + #[account( + mut, + close = budget, + seeds = [ + b"instruction_account_payer".as_ref(), + account.key().as_ref() + ], + bump = account.bumps.payer + )] + pub account_payer: Box>, } pub fn handle(ctx: Context) -> Result<()> { diff --git a/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/mod.rs b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/mod.rs index cdb6979f..0140ff7e 100644 --- a/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/mod.rs +++ b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/mod.rs @@ -1,7 +1,15 @@ +pub mod clear_instruction_account_close; pub mod create_instruction_account; pub mod delete_instruction_account; +pub mod set_instruction_account_close; +pub mod set_instruction_account_collection; +pub mod set_instruction_account_payer; pub mod update_instruction_account; +pub use clear_instruction_account_close::*; pub use create_instruction_account::*; pub use delete_instruction_account::*; +pub use set_instruction_account_close::*; +pub use set_instruction_account_collection::*; +pub use set_instruction_account_payer::*; pub use update_instruction_account::*; diff --git a/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/set_instruction_account_close.rs b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/set_instruction_account_close.rs new file mode 100644 index 00000000..5ea1586f --- /dev/null +++ b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/set_instruction_account_close.rs @@ -0,0 +1,69 @@ +use crate::collections::{ + Collaborator, Instruction, InstructionAccount, InstructionAccountClose, User, Workspace, +}; +use crate::enums::{AccountModifiers, CollaboratorStatus}; +use crate::errors::ErrorCode; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct SetInstructionAccountClose<'info> { + pub authority: Signer<'info>, + pub workspace: Box>, + #[account( + constraint = instruction.workspace == workspace.key() @ ErrorCode::InstructionDoesNotBelongToWorkspace, + )] + pub instruction: Box>, + #[account( + constraint = close.workspace == workspace.key() @ ErrorCode::InstructionAccountDoesNotBelongToWorkspace, + constraint = close.instruction == instruction.key() @ ErrorCode::InstructionAccountDoesNotBelongToInstruction, + )] + pub close: Box>, + #[account( + constraint = account.workspace == workspace.key() @ ErrorCode::InstructionAccountDoesNotBelongToWorkspace, + constraint = account.instruction == instruction.key() @ ErrorCode::InstructionAccountDoesNotBelongToInstruction, + )] + pub account: Box>, + #[account( + mut, + seeds = [ + b"instruction_account_close".as_ref(), + account.key().as_ref(), + ], + bump = account.bumps.close + )] + pub account_close: Box>, + #[account( + seeds = [ + b"user".as_ref(), + authority.key().as_ref(), + ], + bump = user.bump + )] + pub user: Box>, + #[account( + seeds = [ + b"collaborator".as_ref(), + workspace.key().as_ref(), + user.key().as_ref(), + ], + bump = collaborator.bump, + constraint = collaborator.status == CollaboratorStatus::Approved { id: 1 } @ ErrorCode::CollaboratorStatusNotApproved, + )] + pub collaborator: Box>, +} + +pub fn validate(ctx: &Context) -> Result { + match ctx.accounts.account.modifier { + Some(AccountModifiers::Mut { id: 1 }) => Ok(true), + _ => Err(error!(ErrorCode::OnlyMutAccountsCanHaveClose)), + } +} + +pub fn handle(ctx: Context) -> Result<()> { + msg!("Set instruction account close"); + ctx + .accounts + .account_close + .set(Some(ctx.accounts.close.key())); + Ok(()) +} diff --git a/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/set_instruction_account_collection.rs b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/set_instruction_account_collection.rs new file mode 100644 index 00000000..e36d5dae --- /dev/null +++ b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/set_instruction_account_collection.rs @@ -0,0 +1,76 @@ +use crate::collections::{ + Application, Collaborator, Collection, Instruction, InstructionAccount, + InstructionAccountCollection, User, Workspace, +}; +use crate::enums::{AccountKinds, CollaboratorStatus}; +use crate::errors::ErrorCode; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct SetInstructionAccountCollection<'info> { + pub authority: Signer<'info>, + pub workspace: Box>, + #[account( + constraint = application.workspace == workspace.key() @ ErrorCode::ApplicationDoesNotBelongToWorkspace, + )] + pub application: Box>, + #[account( + constraint = instruction.workspace == workspace.key() @ ErrorCode::InstructionDoesNotBelongToWorkspace, + constraint = instruction.application == application.key() @ ErrorCode::InstructionDoesNotBelongToApplication, +)] + pub instruction: Box>, + #[account( + constraint = collection.workspace == workspace.key() @ ErrorCode::CollectionDoesNotBelongToWorkspace, + constraint = collection.application == application.key() @ ErrorCode::CollectionDoesNotBelongToApplication, + )] + pub collection: Box>, + #[account( + constraint = account.workspace == workspace.key() @ ErrorCode::InstructionAccountDoesNotBelongToWorkspace, + constraint = account.application == application.key() @ ErrorCode::InstructionAccountDoesNotBelongToApplication, + constraint = account.instruction == instruction.key() @ ErrorCode::InstructionAccountDoesNotBelongToInstruction, + )] + pub account: Box>, + #[account( + mut, + seeds = [ + b"instruction_account_collection".as_ref(), + account.key().as_ref(), + ], + bump = account.bumps.collection + )] + pub account_collection: Box>, + #[account( + seeds = [ + b"user".as_ref(), + authority.key().as_ref(), + ], + bump = user.bump + )] + pub user: Box>, + #[account( + seeds = [ + b"collaborator".as_ref(), + workspace.key().as_ref(), + user.key().as_ref(), + ], + bump = collaborator.bump, + constraint = collaborator.status == CollaboratorStatus::Approved { id: 1 } @ ErrorCode::CollaboratorStatusNotApproved, + )] + pub collaborator: Box>, +} + +pub fn validate(ctx: &Context) -> Result { + match ctx.accounts.account.kind { + AccountKinds::Document { id: 0 } => Ok(true), + _ => Err(error!(ErrorCode::OnlyDocumentAccountsCanHaveCollection)), + } +} + +pub fn handle(ctx: Context) -> Result<()> { + msg!("Set instruction account collection"); + ctx + .accounts + .account_collection + .set(Some(ctx.accounts.collection.key())); + Ok(()) +} diff --git a/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/set_instruction_account_payer.rs b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/set_instruction_account_payer.rs new file mode 100644 index 00000000..281e3661 --- /dev/null +++ b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/set_instruction_account_payer.rs @@ -0,0 +1,69 @@ +use crate::collections::{ + Collaborator, Instruction, InstructionAccount, InstructionAccountPayer, User, Workspace, +}; +use crate::enums::{AccountModifiers, CollaboratorStatus}; +use crate::errors::ErrorCode; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct SetInstructionAccountPayer<'info> { + pub authority: Signer<'info>, + pub workspace: Box>, + #[account( + constraint = instruction.workspace == workspace.key() @ ErrorCode::InstructionDoesNotBelongToWorkspace, + )] + pub instruction: Box>, + #[account( + constraint = payer.workspace == workspace.key() @ ErrorCode::InstructionAccountDoesNotBelongToWorkspace, + constraint = payer.instruction == instruction.key() @ ErrorCode::InstructionAccountDoesNotBelongToInstruction, + )] + pub payer: Box>, + #[account( + constraint = account.workspace == workspace.key() @ ErrorCode::InstructionAccountDoesNotBelongToWorkspace, + constraint = account.instruction == instruction.key() @ ErrorCode::InstructionAccountDoesNotBelongToInstruction, + )] + pub account: Box>, + #[account( + mut, + seeds = [ + b"instruction_account_payer".as_ref(), + account.key().as_ref(), + ], + bump = account.bumps.payer + )] + pub account_payer: Box>, + #[account( + seeds = [ + b"user".as_ref(), + authority.key().as_ref(), + ], + bump = user.bump + )] + pub user: Box>, + #[account( + seeds = [ + b"collaborator".as_ref(), + workspace.key().as_ref(), + user.key().as_ref(), + ], + bump = collaborator.bump, + constraint = collaborator.status == CollaboratorStatus::Approved { id: 1 } @ ErrorCode::CollaboratorStatusNotApproved, + )] + pub collaborator: Box>, +} + +pub fn validate(ctx: &Context) -> Result { + match ctx.accounts.account.modifier { + Some(AccountModifiers::Init { id: 0 }) => Ok(true), + _ => Err(error!(ErrorCode::OnlyInitAccountsCanHavePayer)), + } +} + +pub fn handle(ctx: Context) -> Result<()> { + msg!("Set instruction account payer"); + ctx + .accounts + .account_payer + .set(Some(ctx.accounts.payer.key())); + Ok(()) +} diff --git a/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/update_instruction_account.rs b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/update_instruction_account.rs index d9281238..65b98550 100644 --- a/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/update_instruction_account.rs +++ b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_account/update_instruction_account.rs @@ -1,15 +1,14 @@ use crate::collections::{ - Collaborator, Collection, Instruction, InstructionAccount, User, Workspace, + Collaborator, Instruction, InstructionAccount, InstructionAccountClose, InstructionAccountPayer, + User, Workspace, }; -use crate::enums::{AccountKinds, AccountModifiers, CollaboratorStatus}; +use crate::enums::{AccountModifiers, CollaboratorStatus}; use crate::errors::ErrorCode; -use crate::utils::{get_account_key, get_remaining_account}; use anchor_lang::prelude::*; #[derive(AnchorSerialize, AnchorDeserialize)] pub struct UpdateInstructionAccountArguments { pub name: String, - pub kind: u8, pub modifier: Option, pub space: Option, } @@ -47,22 +46,29 @@ pub struct UpdateInstructionAccount<'info> { constraint = collaborator.status == CollaboratorStatus::Approved { id: 1 } @ ErrorCode::CollaboratorStatusNotApproved, )] pub collaborator: Box>, + #[account( + mut, + seeds = [ + b"instruction_account_payer".as_ref(), + account.key().as_ref(), + ], + bump = account.bumps.payer + )] + pub account_payer: Box>, + #[account( + mut, + seeds = [ + b"instruction_account_close".as_ref(), + account.key().as_ref(), + ], + bump = account.bumps.close + )] + pub account_close: Box>, } -pub fn validate( - ctx: &Context, - arguments: &UpdateInstructionAccountArguments, -) -> Result { - match ( - arguments.kind, - get_remaining_account::(ctx.remaining_accounts, 0)?, - arguments.modifier, - arguments.space, - get_remaining_account::(ctx.remaining_accounts, 1)?, - ) { - (0, None, _, _, _) => Err(error!(ErrorCode::MissingCollectionAccount)), - (_, _, Some(0), None, _) => Err(error!(ErrorCode::MissingSpace)), - (_, _, Some(0), _, None) => Err(error!(ErrorCode::MissingPayerAccount)), +pub fn validate(arguments: &UpdateInstructionAccountArguments) -> Result { + match (arguments.modifier, arguments.space) { + (Some(0), None) => Err(error!(ErrorCode::MissingSpace)), _ => Ok(true), } } @@ -73,27 +79,19 @@ pub fn handle( ) -> Result<()> { msg!("Update instruction account"); ctx.accounts.account.rename(arguments.name); - ctx.accounts.account.change_settings( - AccountKinds::create( - arguments.kind, - get_account_key(get_remaining_account::( - ctx.remaining_accounts, - 0, - )?)?, - )?, - AccountModifiers::create( - arguments.modifier, - arguments.space, - get_account_key(get_remaining_account::( - ctx.remaining_accounts, - 1, - )?)?, - get_account_key(get_remaining_account::( - ctx.remaining_accounts, - 1, - )?)?, - )?, - ); + ctx + .accounts + .account + .set_modifier(AccountModifiers::create(arguments.modifier)?); + ctx.accounts.account.set_space(arguments.space); ctx.accounts.account.bump_timestamp()?; + match (ctx.accounts.account_payer.payer, arguments.modifier) { + (_, Some(0)) => {} + _ => ctx.accounts.account_payer.set(None), + }; + match (ctx.accounts.account_close.close, arguments.modifier) { + (_, Some(1)) => {} + _ => ctx.accounts.account_close.set(None), + }; Ok(()) } diff --git a/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_argument/create_instruction_argument.rs b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_argument/create_instruction_argument.rs index bf3037a6..8cf7d915 100644 --- a/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_argument/create_instruction_argument.rs +++ b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_argument/create_instruction_argument.rs @@ -4,7 +4,7 @@ use crate::collections::{ }; use crate::enums::{AttributeKinds, AttributeModifiers, CollaboratorStatus}; use crate::errors::ErrorCode; -use crate::utils::{fund_rent_for_account, has_enough_funds}; +use crate::utils::{has_enough_funds, transfer_lamports}; use anchor_lang::prelude::*; #[derive(AnchorSerialize, AnchorDeserialize)] @@ -100,7 +100,7 @@ pub fn handle( arguments: CreateInstructionArgumentArguments, ) -> Result<()> { msg!("Create instruction argument"); - fund_rent_for_account( + transfer_lamports( ctx.accounts.budget.to_account_info(), ctx.accounts.authority.to_account_info(), **ctx.accounts.argument.to_account_info().lamports.borrow(), diff --git a/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_relation/create_instruction_relation.rs b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_relation/create_instruction_relation.rs index 52b02418..1ca750de 100644 --- a/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_relation/create_instruction_relation.rs +++ b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_relation/create_instruction_relation.rs @@ -4,7 +4,7 @@ use crate::collections::{ }; use crate::enums::CollaboratorStatus; use crate::errors::ErrorCode; -use crate::utils::{fund_rent_for_account, has_enough_funds}; +use crate::utils::{has_enough_funds, transfer_lamports}; use anchor_lang::prelude::*; #[derive(Accounts)] @@ -76,22 +76,20 @@ pub struct CreateInstructionRelation<'info> { pub budget: Box>, #[account( mut, - constraint = from_stats.quantity_of_relations == 0 @ ErrorCode::CantDeleteAccountWithRelations, seeds = [ b"instruction_account_stats".as_ref(), from.key().as_ref() ], - bump = from.instruction_account_stats_bump + bump = from.bumps.stats )] pub from_stats: Box>, #[account( mut, - constraint = to_stats.quantity_of_relations == 0 @ ErrorCode::CantDeleteAccountWithRelations, seeds = [ b"instruction_account_stats".as_ref(), to.key().as_ref() ], - bump = to.instruction_account_stats_bump + bump = to.bumps.stats )] pub to_stats: Box>, } @@ -110,7 +108,7 @@ pub fn validate(ctx: &Context) -> Result { pub fn handle(ctx: Context) -> Result<()> { msg!("Create instruction relation"); - fund_rent_for_account( + transfer_lamports( ctx.accounts.budget.to_account_info(), ctx.accounts.authority.to_account_info(), **ctx.accounts.relation.to_account_info().lamports.borrow(), diff --git a/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_relation/delete_instruction_relation.rs b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_relation/delete_instruction_relation.rs index c8e9672c..80be7420 100644 --- a/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_relation/delete_instruction_relation.rs +++ b/apps/bulldozer-programs/programs/bulldozer/src/instructions/instruction_relation/delete_instruction_relation.rs @@ -26,7 +26,7 @@ pub struct DeleteInstructionRelation<'info> { pub to: Box>, #[account( mut, - close = authority, + close = budget, seeds = [ b"instruction_relation".as_ref(), from.key().as_ref(), @@ -68,7 +68,7 @@ pub struct DeleteInstructionRelation<'info> { b"instruction_account_stats".as_ref(), from.key().as_ref() ], - bump = from.instruction_account_stats_bump + bump = from.bumps.stats )] pub from_stats: Box>, #[account( @@ -77,7 +77,7 @@ pub struct DeleteInstructionRelation<'info> { b"instruction_account_stats".as_ref(), to.key().as_ref() ], - bump = to.instruction_account_stats_bump + bump = to.bumps.stats )] pub to_stats: Box>, } diff --git a/apps/bulldozer-programs/programs/bulldozer/src/instructions/user/create_user.rs b/apps/bulldozer-programs/programs/bulldozer/src/instructions/user/create_user.rs index 5fa02542..abf83508 100644 --- a/apps/bulldozer-programs/programs/bulldozer/src/instructions/user/create_user.rs +++ b/apps/bulldozer-programs/programs/bulldozer/src/instructions/user/create_user.rs @@ -1,6 +1,13 @@ use crate::collections::User; use anchor_lang::prelude::*; +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct CreateUserArguments { + user_name: String, + name: String, + thumbnail_url: String, +} + #[derive(Accounts)] pub struct CreateUser<'info> { #[account( @@ -19,12 +26,15 @@ pub struct CreateUser<'info> { pub system_program: Program<'info, System>, } -pub fn handle(ctx: Context) -> Result<()> { +pub fn handle(ctx: Context, arguments: CreateUserArguments) -> Result<()> { msg!("Create user"); - ctx - .accounts - .user - .initialize(*ctx.accounts.authority.key, *ctx.bumps.get("user").unwrap()); + ctx.accounts.user.initialize( + *ctx.accounts.authority.key, + arguments.user_name.to_string(), + arguments.name.to_string(), + arguments.thumbnail_url.to_string(), + *ctx.bumps.get("user").unwrap(), + ); ctx.accounts.user.initialize_timestamp()?; Ok(()) } diff --git a/apps/bulldozer-programs/programs/bulldozer/src/instructions/user/mod.rs b/apps/bulldozer-programs/programs/bulldozer/src/instructions/user/mod.rs index 312fc0c4..d5b1223b 100644 --- a/apps/bulldozer-programs/programs/bulldozer/src/instructions/user/mod.rs +++ b/apps/bulldozer-programs/programs/bulldozer/src/instructions/user/mod.rs @@ -1,5 +1,7 @@ pub mod create_user; pub mod delete_user; +pub mod update_user; pub use create_user::*; pub use delete_user::*; +pub use update_user::*; diff --git a/apps/bulldozer-programs/programs/bulldozer/src/instructions/user/update_user.rs b/apps/bulldozer-programs/programs/bulldozer/src/instructions/user/update_user.rs new file mode 100644 index 00000000..8d3654ff --- /dev/null +++ b/apps/bulldozer-programs/programs/bulldozer/src/instructions/user/update_user.rs @@ -0,0 +1,35 @@ +use crate::collections::User; +use anchor_lang::prelude::*; + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct UpdateUserArguments { + user_name: String, + name: String, + thumbnail_url: String, +} + +#[derive(Accounts)] +pub struct UpdateUser<'info> { + #[account( + mut, + has_one = authority, + seeds = [ + b"user".as_ref(), + authority.key().as_ref() + ], + bump = user.bump + )] + pub user: Box>, + pub authority: Signer<'info>, +} + +pub fn handle(ctx: Context, arguments: UpdateUserArguments) -> Result<()> { + msg!("Update user"); + ctx.accounts.user.update( + arguments.user_name.to_string(), + arguments.name.to_string(), + arguments.thumbnail_url.to_string(), + ); + ctx.accounts.user.bump_timestamp()?; + Ok(()) +} diff --git a/apps/bulldozer-programs/programs/bulldozer/src/instructions/workspace/deposit_to_budget.rs b/apps/bulldozer-programs/programs/bulldozer/src/instructions/workspace/deposit_to_budget.rs new file mode 100644 index 00000000..66db8f85 --- /dev/null +++ b/apps/bulldozer-programs/programs/bulldozer/src/instructions/workspace/deposit_to_budget.rs @@ -0,0 +1,43 @@ +use crate::collections::{Budget, Workspace}; +use anchor_lang::prelude::*; + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct DepositToBudgetArguments { + pub amount: u64, +} + +#[derive(Accounts)] +#[instruction(arguments: DepositToBudgetArguments)] +pub struct DepositToBudget<'info> { + pub system_program: Program<'info, System>, + #[account(mut)] + pub authority: Signer<'info>, + pub workspace: Box>, + #[account( + mut, + seeds = [ + b"budget".as_ref(), + workspace.key().as_ref(), + ], + bump = budget.bump, + )] + pub budget: Box>, +} + +pub fn handle(ctx: Context, arguments: DepositToBudgetArguments) -> Result<()> { + msg!("Deposit to budget"); + anchor_lang::solana_program::program::invoke( + &anchor_lang::solana_program::system_instruction::transfer( + &ctx.accounts.authority.key(), + &ctx.accounts.budget.key(), + arguments.amount, + ), + &[ + ctx.accounts.authority.to_account_info().clone(), + ctx.accounts.budget.to_account_info().clone(), + ctx.accounts.system_program.to_account_info().clone(), + ], + )?; + ctx.accounts.budget.deposit(arguments.amount); + Ok(()) +} diff --git a/apps/bulldozer-programs/programs/bulldozer/src/instructions/workspace/mod.rs b/apps/bulldozer-programs/programs/bulldozer/src/instructions/workspace/mod.rs index c8d29eef..cc569aa9 100644 --- a/apps/bulldozer-programs/programs/bulldozer/src/instructions/workspace/mod.rs +++ b/apps/bulldozer-programs/programs/bulldozer/src/instructions/workspace/mod.rs @@ -1,7 +1,11 @@ pub mod create_workspace; pub mod delete_workspace; +pub mod deposit_to_budget; pub mod update_workspace; +pub mod withdraw_from_budget; pub use create_workspace::*; pub use delete_workspace::*; +pub use deposit_to_budget::*; pub use update_workspace::*; +pub use withdraw_from_budget::*; diff --git a/apps/bulldozer-programs/programs/bulldozer/src/instructions/workspace/withdraw_from_budget.rs b/apps/bulldozer-programs/programs/bulldozer/src/instructions/workspace/withdraw_from_budget.rs new file mode 100644 index 00000000..96f96bea --- /dev/null +++ b/apps/bulldozer-programs/programs/bulldozer/src/instructions/workspace/withdraw_from_budget.rs @@ -0,0 +1,41 @@ +use crate::collections::{Budget, Workspace}; +use crate::errors::ErrorCode; +use crate::utils::transfer_lamports; +use anchor_lang::prelude::*; + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct WithdrawFromBudgetArguments { + pub amount: u64, +} + +#[derive(Accounts)] +#[instruction(arguments: WithdrawFromBudgetArguments)] +pub struct WithdrawFromBudget<'info> { + #[account(mut)] + pub authority: Signer<'info>, + #[account(has_one = authority @ ErrorCode::UnauthorizedWithdraw)] + pub workspace: Box>, + #[account( + mut, + seeds = [ + b"budget".as_ref(), + workspace.key().as_ref(), + ], + bump = budget.bump, + )] + pub budget: Box>, +} + +pub fn handle( + ctx: Context, + arguments: WithdrawFromBudgetArguments, +) -> Result<()> { + msg!("Withdraw from budget"); + transfer_lamports( + ctx.accounts.budget.to_account_info(), + ctx.accounts.authority.to_account_info(), + arguments.amount, + )?; + ctx.accounts.budget.withdraw(arguments.amount); + Ok(()) +} diff --git a/apps/bulldozer-programs/programs/bulldozer/src/lib.rs b/apps/bulldozer-programs/programs/bulldozer/src/lib.rs index 361da264..b3ff9e6a 100644 --- a/apps/bulldozer-programs/programs/bulldozer/src/lib.rs +++ b/apps/bulldozer-programs/programs/bulldozer/src/lib.rs @@ -14,8 +14,12 @@ declare_id!("EYpJuu7FLtQAHXFY7vcCihRjAyBjb31HCGaJgo1c3fEo"); pub mod bulldozer { use super::*; - pub fn create_user(ctx: Context) -> Result<()> { - instructions::create_user::handle(ctx) + pub fn create_user(ctx: Context, arguments: CreateUserArguments) -> Result<()> { + instructions::create_user::handle(ctx, arguments) + } + + pub fn update_user(ctx: Context, arguments: UpdateUserArguments) -> Result<()> { + instructions::update_user::handle(ctx, arguments) } pub fn delete_user(ctx: Context) -> Result<()> { @@ -36,6 +40,20 @@ pub mod bulldozer { instructions::update_workspace::handle(ctx, arguments) } + pub fn deposit_to_budget( + ctx: Context, + arguments: DepositToBudgetArguments, + ) -> Result<()> { + instructions::deposit_to_budget::handle(ctx, arguments) + } + + pub fn withdraw_from_budget( + ctx: Context, + arguments: WithdrawFromBudgetArguments, + ) -> Result<()> { + instructions::withdraw_from_budget::handle(ctx, arguments) + } + pub fn delete_workspace(ctx: Context) -> Result<()> { instructions::delete_workspace::handle(ctx) } @@ -177,7 +195,7 @@ pub mod bulldozer { instructions::create_instruction_account::handle(ctx, arguments) } - #[access_control(instructions::update_instruction_account::validate(&ctx, &arguments))] + #[access_control(instructions::update_instruction_account::validate(&arguments))] pub fn update_instruction_account( ctx: Context, arguments: UpdateInstructionAccountArguments, @@ -185,6 +203,27 @@ pub mod bulldozer { instructions::update_instruction_account::handle(ctx, arguments) } + #[access_control(instructions::set_instruction_account_collection::validate(&ctx))] + pub fn set_instruction_account_collection( + ctx: Context, + ) -> Result<()> { + instructions::set_instruction_account_collection::handle(ctx) + } + + #[access_control(instructions::set_instruction_account_close::validate(&ctx))] + pub fn set_instruction_account_close(ctx: Context) -> Result<()> { + instructions::set_instruction_account_close::handle(ctx) + } + + pub fn clear_instruction_account_close(ctx: Context) -> Result<()> { + instructions::clear_instruction_account_close::handle(ctx) + } + + #[access_control(instructions::set_instruction_account_payer::validate(&ctx))] + pub fn set_instruction_account_payer(ctx: Context) -> Result<()> { + instructions::set_instruction_account_payer::handle(ctx) + } + pub fn delete_instruction_account(ctx: Context) -> Result<()> { instructions::delete_instruction_account::handle(ctx) } diff --git a/apps/bulldozer-programs/programs/bulldozer/src/utils.rs b/apps/bulldozer-programs/programs/bulldozer/src/utils.rs index 4ea76a27..15c80a10 100644 --- a/apps/bulldozer-programs/programs/bulldozer/src/utils.rs +++ b/apps/bulldozer-programs/programs/bulldozer/src/utils.rs @@ -1,36 +1,19 @@ use crate::errors::ErrorCode; use anchor_lang::prelude::*; -pub fn get_remaining_account<'info, T: AccountSerialize + AccountDeserialize + Owner + Clone>( - remaining_accounts: &[AccountInfo<'info>], - index: usize, -) -> std::result::Result>, Error> { - let maybe_account: Option<&AccountInfo> = remaining_accounts.get(index); - let maybe_decoded_account: Option, Error>> = - maybe_account.map(Account::try_from); - match maybe_decoded_account { - Some(Ok(account)) => Ok(Some(account)), - Some(Err(_)) => return Err(error!(ErrorCode::InvalidAccount)), - None => Ok(None), - } -} - -pub fn get_account_key<'info, T: AccountSerialize + AccountDeserialize + Owner + Clone>( - maybe_account: Option>, -) -> std::result::Result, Error> { - match maybe_account { - Some(account) => Ok(Some(account.key())), - _ => Ok(None), - } -} - -pub fn fund_rent_for_account<'info>( - payer: AccountInfo<'info>, - receiver: AccountInfo<'info>, - rent: u64, +pub fn transfer_lamports<'info>( + from: AccountInfo<'info>, + to: AccountInfo<'info>, + lamports: u64, ) -> Result<()> { - **payer.try_borrow_mut_lamports()? -= rent; - **receiver.try_borrow_mut_lamports()? += rent; + **from.try_borrow_mut_lamports()? = from + .try_lamports()? + .checked_sub(lamports) + .ok_or(ErrorCode::ArithmeticError)?; + **to.try_borrow_mut_lamports()? = to + .try_lamports()? + .checked_add(lamports) + .ok_or(ErrorCode::ArithmeticError)?; Ok(()) } diff --git a/apps/bulldozer-programs/tests/application.spec.ts b/apps/bulldozer-programs/tests/application.spec.ts index 1471ebd2..b1745a54 100644 --- a/apps/bulldozer-programs/tests/application.spec.ts +++ b/apps/bulldozer-programs/tests/application.spec.ts @@ -1,4 +1,4 @@ -import { Program, ProgramError, Provider } from '@heavy-duty/anchor'; +import { AnchorError, AnchorProvider, Program } from '@heavy-duty/anchor'; import { Keypair, LAMPORTS_PER_SOL, @@ -10,17 +10,20 @@ import { Bulldozer, IDL } from '../target/types/bulldozer'; import { BULLDOZER_PROGRAM_ID } from './utils'; describe('application', () => { - const program = new Program( - IDL, - BULLDOZER_PROGRAM_ID, - Provider.env() - ); + const provider = AnchorProvider.env(); + const program = new Program(IDL, BULLDOZER_PROGRAM_ID, provider); const workspaceName = 'my-workspace'; const workspace = Keypair.generate(); const application = Keypair.generate(); const applicationName = 'my-app'; let budgetPublicKey: PublicKey; let workspaceStatsPublicKey: PublicKey; + const userUserName = 'user-name-1'; + const userName = 'User Name 1'; + const userThumbnailUrl = 'https://img/1.com'; + const newUserUserName = 'user-name-2'; + const newUserName = 'User Name 2'; + const newUserThumbnailUrl = 'https://img/2.com'; before(async () => { [budgetPublicKey] = await PublicKey.findProgramAddress( @@ -34,9 +37,13 @@ describe('application', () => { try { await program.methods - .createUser() + .createUser({ + name: userName, + thumbnailUrl: userThumbnailUrl, + userName: userUserName, + }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, }) .rpc(); } catch (error) {} @@ -44,13 +51,13 @@ describe('application', () => { await program.methods .createWorkspace({ name: workspaceName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, }) .signers([workspace]) .postInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: budgetPublicKey, lamports: LAMPORTS_PER_SOL, }), @@ -66,7 +73,7 @@ describe('application', () => { }) .accounts({ application: application.publicKey, - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, }) .signers([application]) @@ -78,7 +85,7 @@ describe('application', () => { const workspaceStatsAccount = await program.account.workspaceStats.fetch( workspaceStatsPublicKey ); - assert.ok(account.authority.equals(program.provider.wallet.publicKey)); + assert.ok(account.authority.equals(provider.wallet.publicKey)); assert.ok(account.workspace.equals(workspace.publicKey)); assert.equal(account.name, applicationName); assert.equal(workspaceStatsAccount.quantityOfApplications, 1); @@ -92,7 +99,7 @@ describe('application', () => { await program.methods .updateApplication({ name: applicationName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, application: application.publicKey, workspace: workspace.publicKey, }) @@ -110,7 +117,7 @@ describe('application', () => { await program.methods .deleteApplication() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, application: application.publicKey, workspace: workspace.publicKey, }) @@ -132,7 +139,7 @@ describe('application', () => { const newApplication = Keypair.generate(); const collectionName = 'sample'; const collection = Keypair.generate(); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods @@ -140,14 +147,14 @@ describe('application', () => { .accounts({ application: newApplication.publicKey, workspace: workspace.publicKey, - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, }) .signers([newApplication]) .rpc(); await program.methods .createCollection({ name: collectionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: newApplication.publicKey, collection: collection.publicKey, @@ -157,16 +164,16 @@ describe('application', () => { await program.methods .deleteApplication() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, application: newApplication.publicKey, workspace: workspace.publicKey, }) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6020); + assert.equal(error?.error.errorCode.number, 6020); }); it('should fail when deleting application with instructions', async () => { @@ -175,7 +182,7 @@ describe('application', () => { const newApplication = Keypair.generate(); const instructionName = 'sample'; const instruction = Keypair.generate(); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods @@ -183,14 +190,14 @@ describe('application', () => { .accounts({ application: newApplication.publicKey, workspace: workspace.publicKey, - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, }) .signers([newApplication]) .rpc(); await program.methods .createInstruction({ name: instructionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: newApplication.publicKey, instruction: instruction.publicKey, @@ -200,16 +207,16 @@ describe('application', () => { await program.methods .deleteApplication() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, application: newApplication.publicKey, workspace: workspace.publicKey, }) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6022); + assert.equal(error?.error.errorCode.number, 6022); }); it('should fail when providing wrong "workspace" to delete', async () => { @@ -222,19 +229,19 @@ describe('application', () => { [Buffer.from('budget', 'utf8'), newWorkspace.publicKey.toBuffer()], program.programId ); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods .createWorkspace({ name: newWorkspaceName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, }) .signers([newWorkspace]) .postInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newBudgetPublicKey, lamports: LAMPORTS_PER_SOL, }), @@ -243,7 +250,7 @@ describe('application', () => { await program.methods .createApplication({ name: newApplicationName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, application: newApplication.publicKey, }) @@ -252,16 +259,16 @@ describe('application', () => { await program.methods .deleteApplication() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: newApplication.publicKey, }) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6033); + assert.equal(error?.error.errorCode.number, 6033); }); it('should fail when workspace has insufficient funds', async () => { @@ -270,13 +277,13 @@ describe('application', () => { const newWorkspaceName = 'sample'; const newApplication = Keypair.generate(); const newApplicationName = 'sample'; - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods .createWorkspace({ name: newWorkspaceName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, }) .signers([newWorkspace]) @@ -284,17 +291,17 @@ describe('application', () => { await program.methods .createApplication({ name: newApplicationName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, application: newApplication.publicKey, }) .signers([newApplication]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6027); + assert.equal(error?.error.errorCode.number, 6027); }); it('should fail when user is not a collaborator', async () => { @@ -302,7 +309,7 @@ describe('application', () => { const newApplication = Keypair.generate(); const newApplicationName = 'sample'; const newUser = Keypair.generate(); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods @@ -315,17 +322,17 @@ describe('application', () => { .signers([newUser, newApplication]) .preInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newUser.publicKey, lamports: LAMPORTS_PER_SOL, }), ]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 3012); + assert.equal(error?.error.errorCode.number, 3012); }); it('should fail when user is not an approved collaborator', async () => { @@ -333,21 +340,25 @@ describe('application', () => { const newApplication = Keypair.generate(); const newApplicationName = 'sample'; const newUser = Keypair.generate(); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act const [newUserPublicKey] = await PublicKey.findProgramAddress( [Buffer.from('user', 'utf8'), newUser.publicKey.toBuffer()], program.programId ); await program.methods - .createUser() + .createUser({ + name: newUserName, + thumbnailUrl: newUserThumbnailUrl, + userName: newUserUserName, + }) .accounts({ authority: newUser.publicKey, }) .signers([newUser]) .preInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newUser.publicKey, lamports: LAMPORTS_PER_SOL, }), @@ -374,9 +385,9 @@ describe('application', () => { .signers([newUser, newApplication]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6029); + assert.equal(error?.error.errorCode.number, 6029); }); }); diff --git a/apps/bulldozer-programs/tests/budget.spec.ts b/apps/bulldozer-programs/tests/budget.spec.ts new file mode 100644 index 00000000..2d75fceb --- /dev/null +++ b/apps/bulldozer-programs/tests/budget.spec.ts @@ -0,0 +1,162 @@ +import { AnchorError, AnchorProvider, BN, Program } from '@heavy-duty/anchor'; +import { + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + SystemProgram, +} from '@solana/web3.js'; +import { assert } from 'chai'; +import { Bulldozer, IDL } from '../target/types/bulldozer'; +import { BULLDOZER_PROGRAM_ID } from './utils'; + +describe('workspace', () => { + const provider = AnchorProvider.env(); + const program = new Program(IDL, BULLDOZER_PROGRAM_ID, provider); + const workspace = Keypair.generate(); + const newUser = Keypair.generate(); + let userPublicKey: PublicKey; + let budgetPublicKey: PublicKey; + const userUserName = 'user-name-1'; + const userName = 'User Name 1'; + const userThumbnailUrl = 'https://img/1.com'; + const newUserUserName = 'user-name-2'; + const newUserName = 'User Name 2'; + const newUserThumbnailUrl = 'https://img/2.com'; + const workspaceName = 'my-app'; + + before(async () => { + [userPublicKey] = await PublicKey.findProgramAddress( + [Buffer.from('user', 'utf8'), provider.wallet.publicKey.toBuffer()], + program.programId + ); + [budgetPublicKey] = await PublicKey.findProgramAddress( + [Buffer.from('budget', 'utf8'), workspace.publicKey.toBuffer()], + program.programId + ); + try { + await program.methods + .createUser({ + name: userName, + thumbnailUrl: userThumbnailUrl, + userName: userUserName, + }) + .accounts({ + authority: provider.wallet.publicKey, + }) + .rpc(); + } catch (error) {} + + await program.methods + .createWorkspace({ + name: workspaceName, + }) + .accounts({ + authority: provider.wallet.publicKey, + workspace: workspace.publicKey, + }) + .signers([workspace]) + .rpc(); + + await program.methods + .createUser({ + name: newUserName, + thumbnailUrl: newUserThumbnailUrl, + userName: newUserUserName, + }) + .accounts({ + authority: newUser.publicKey, + }) + .signers([newUser]) + .preInstructions([ + SystemProgram.transfer({ + fromPubkey: provider.wallet.publicKey, + toPubkey: newUser.publicKey, + lamports: LAMPORTS_PER_SOL, + }), + ]) + .rpc(); + }); + + it('should deposit to budget', async () => { + // arrange + const amount = new BN(LAMPORTS_PER_SOL); + const budgetBefore = await provider.connection.getAccountInfo( + budgetPublicKey + ); + // act + await program.methods + .depositToBudget({ + amount, + }) + .accounts({ + authority: provider.wallet.publicKey, + workspace: workspace.publicKey, + }) + .rpc(); + // assert + const budgetAfter = await provider.connection.getAccountInfo( + budgetPublicKey + ); + const budget = await program.account.budget.fetch(budgetPublicKey); + assert.ok( + new BN(budgetBefore?.lamports ?? 0) + .add(amount) + .eq(new BN(budgetAfter?.lamports ?? 0)) + ); + assert.ok(budget.totalDeposited.eq(amount)); + assert.ok(budget.totalValueLocked.eq(amount)); + }); + + it('should when withdrawing unauthorized', async () => { + // arrange + const amount = new BN(LAMPORTS_PER_SOL); + let error: AnchorError | null = null; + // act + try { + await program.methods + .withdrawFromBudget({ + amount, + }) + .accounts({ + authority: newUser.publicKey, + workspace: workspace.publicKey, + }) + .signers([newUser]) + .rpc(); + } catch (err) { + error = err as AnchorError; + } + // assert + assert.equal(error?.error.errorCode.number, 6050); + }); + + it('should withdraw from budget', async () => { + // arrange + const amount = new BN(LAMPORTS_PER_SOL); + const budgetBefore = await provider.connection.getAccountInfo( + budgetPublicKey + ); + // act + await program.methods + .withdrawFromBudget({ + amount, + }) + .accounts({ + authority: provider.wallet.publicKey, + workspace: workspace.publicKey, + }) + .rpc(); + // assert + const budgetAfter = await provider.connection.getAccountInfo( + budgetPublicKey + ); + const budget = await program.account.budget.fetch(budgetPublicKey); + assert.ok( + new BN(budgetBefore?.lamports ?? 0) + .sub(amount) + .eq(new BN(budgetAfter?.lamports ?? 0)) + ); + assert.ok(budget.totalDeposited.eq(amount)); + assert.ok(budget.totalValueLocked.eq(new BN(0))); + }); +}); diff --git a/apps/bulldozer-programs/tests/collaborator.spec.ts b/apps/bulldozer-programs/tests/collaborator.spec.ts index c99d8296..3343df29 100644 --- a/apps/bulldozer-programs/tests/collaborator.spec.ts +++ b/apps/bulldozer-programs/tests/collaborator.spec.ts @@ -1,4 +1,4 @@ -import { Program, Provider } from '@heavy-duty/anchor'; +import { AnchorProvider, Program } from '@heavy-duty/anchor'; import { Keypair, LAMPORTS_PER_SOL, @@ -10,11 +10,8 @@ import { Bulldozer, IDL } from '../target/types/bulldozer'; import { BULLDOZER_PROGRAM_ID } from './utils'; describe('collaborator', () => { - const program = new Program( - IDL, - BULLDOZER_PROGRAM_ID, - Provider.env() - ); + const provider = AnchorProvider.env(); + const program = new Program(IDL, BULLDOZER_PROGRAM_ID, provider); const workspace = Keypair.generate(); const newUser = Keypair.generate(); const workspaceName = 'my-app'; @@ -23,13 +20,16 @@ describe('collaborator', () => { let userPublicKey: PublicKey; let newUserPublicKey: PublicKey; let workspaceStatsPublicKey: PublicKey; + const userUserName = 'user-name-1'; + const userName = 'User Name 1'; + const userThumbnailUrl = 'https://img/1.com'; + const newUserUserName = 'user-name-2'; + const newUserName = 'User Name 2'; + const newUserThumbnailUrl = 'https://img/2.com'; before(async () => { [userPublicKey] = await PublicKey.findProgramAddress( - [ - Buffer.from('user', 'utf8'), - program.provider.wallet.publicKey.toBuffer(), - ], + [Buffer.from('user', 'utf8'), provider.wallet.publicKey.toBuffer()], program.programId ); [newUserPublicKey] = await PublicKey.findProgramAddress( @@ -59,15 +59,23 @@ describe('collaborator', () => { try { await program.methods - .createUser() + .createUser({ + name: userName, + thumbnailUrl: userThumbnailUrl, + userName: userUserName, + }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, }) .rpc(); } catch (error) {} await program.methods - .createUser() + .createUser({ + name: newUserName, + thumbnailUrl: newUserThumbnailUrl, + userName: newUserUserName, + }) .accounts({ authority: newUser.publicKey, user: newUserPublicKey, @@ -75,7 +83,7 @@ describe('collaborator', () => { .signers([newUser]) .preInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newUser.publicKey, lamports: LAMPORTS_PER_SOL, }), @@ -85,7 +93,7 @@ describe('collaborator', () => { await program.methods .createWorkspace({ name: workspaceName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, }) .signers([workspace]) @@ -97,9 +105,7 @@ describe('collaborator', () => { const collaboratorAccount = await program.account.collaborator.fetch( collaboratorPublicKey ); - assert.ok( - collaboratorAccount.authority.equals(program.provider.wallet.publicKey) - ); + assert.ok(collaboratorAccount.authority.equals(provider.wallet.publicKey)); assert.equal(collaboratorAccount.isAdmin, true); assert.ok('approved' in collaboratorAccount.status); }); @@ -134,7 +140,7 @@ describe('collaborator', () => { await program.methods .updateCollaborator({ status: 1 }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, collaborator: newCollaboratorPublicKey, }) @@ -151,7 +157,7 @@ describe('collaborator', () => { await program.methods .deleteCollaborator() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, collaborator: newCollaboratorPublicKey, }) @@ -181,7 +187,7 @@ describe('collaborator', () => { await program.methods .updateCollaborator({ status: 2 }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, collaborator: newCollaboratorPublicKey, }) diff --git a/apps/bulldozer-programs/tests/collection-attribute.spec.ts b/apps/bulldozer-programs/tests/collection-attribute.spec.ts index da705d89..7e9e0ff8 100644 --- a/apps/bulldozer-programs/tests/collection-attribute.spec.ts +++ b/apps/bulldozer-programs/tests/collection-attribute.spec.ts @@ -1,4 +1,4 @@ -import { Program, ProgramError, Provider } from '@heavy-duty/anchor'; +import { AnchorError, AnchorProvider, Program } from '@heavy-duty/anchor'; import { Keypair, LAMPORTS_PER_SOL, @@ -10,11 +10,8 @@ import { Bulldozer, IDL } from '../target/types/bulldozer'; import { BULLDOZER_PROGRAM_ID, decodeAttributeEnum } from './utils'; describe('collection attribute', () => { - const program = new Program( - IDL, - BULLDOZER_PROGRAM_ID, - Provider.env() - ); + const provider = AnchorProvider.env(); + const program = new Program(IDL, BULLDOZER_PROGRAM_ID, provider); const attribute = Keypair.generate(); const collection = Keypair.generate(); const application = Keypair.generate(); @@ -24,6 +21,12 @@ describe('collection attribute', () => { const collectionName = 'my-collection'; let collectionStatsPublicKey: PublicKey; let budgetPublicKey: PublicKey; + const userUserName = 'user-name-1'; + const userName = 'User Name 1'; + const userThumbnailUrl = 'https://img/1.com'; + const newUserUserName = 'user-name-2'; + const newUserName = 'User Name 2'; + const newUserThumbnailUrl = 'https://img/2.com'; before(async () => { [budgetPublicKey] = await PublicKey.findProgramAddress( @@ -40,9 +43,13 @@ describe('collection attribute', () => { try { await program.methods - .createUser() + .createUser({ + name: userName, + thumbnailUrl: userThumbnailUrl, + userName: userUserName, + }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, }) .rpc(); } catch (error) {} @@ -50,13 +57,13 @@ describe('collection attribute', () => { await program.methods .createWorkspace({ name: workspaceName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, }) .signers([workspace]) .postInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: budgetPublicKey, lamports: LAMPORTS_PER_SOL, }), @@ -66,7 +73,7 @@ describe('collection attribute', () => { await program.methods .createApplication({ name: applicationName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, }) @@ -79,7 +86,7 @@ describe('collection attribute', () => { collection: collection.publicKey, workspace: workspace.publicKey, application: application.publicKey, - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, }) .signers([collection]) .rpc(); @@ -99,7 +106,7 @@ describe('collection attribute', () => { await program.methods .createCollectionAttribute(attributesData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, collection: collection.publicKey, @@ -117,9 +124,7 @@ describe('collection attribute', () => { collectionAttributeAccount.kind as any ); assert.ok( - collectionAttributeAccount.authority.equals( - program.provider.wallet.publicKey - ) + collectionAttributeAccount.authority.equals(provider.wallet.publicKey) ); assert.equal(collectionAttributeAccount.name, attributesData.name); assert.equal(decodedKind.id, attributesData.kind); @@ -153,7 +158,7 @@ describe('collection attribute', () => { await program.methods .updateCollectionAttribute(attributesData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, collection: collection.publicKey, attribute: attribute.publicKey, @@ -180,7 +185,7 @@ describe('collection attribute', () => { await program.methods .deleteCollectionAttribute() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, collection: collection.publicKey, attribute: attribute.publicKey, @@ -208,13 +213,13 @@ describe('collection attribute', () => { max: null, maxLength: null, }; - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods .createCollectionAttribute(attributesData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, collection: collection.publicKey, @@ -223,10 +228,10 @@ describe('collection attribute', () => { .signers([attribute]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6011); + assert.equal(error?.error.errorCode.number, 6011); }); it('should fail when max length is not provided with a string', async () => { @@ -239,13 +244,13 @@ describe('collection attribute', () => { max: null, maxLength: null, }; - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods .createCollectionAttribute(attributesData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, collection: collection.publicKey, @@ -254,10 +259,10 @@ describe('collection attribute', () => { .signers([attribute]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6012); + assert.equal(error?.error.errorCode.number, 6012); }); it('should fail when providing wrong "collection" to delete', async () => { @@ -273,13 +278,13 @@ describe('collection attribute', () => { max: null, maxLength: null, }; - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods .createCollection({ name: newCollectionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, collection: newCollection.publicKey, @@ -289,7 +294,7 @@ describe('collection attribute', () => { await program.methods .createCollectionAttribute(attributesData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, collection: newCollection.publicKey, @@ -300,17 +305,17 @@ describe('collection attribute', () => { await program.methods .deleteCollectionAttribute() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, collection: collection.publicKey, attribute: newAttribute.publicKey, }) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6037); + assert.equal(error?.error.errorCode.number, 6037); }); it('should fail when workspace has insufficient funds', async () => { @@ -334,30 +339,30 @@ describe('collection attribute', () => { [Buffer.from('budget', 'utf8'), newWorkspace.publicKey.toBuffer()], program.programId ); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act await program.methods .createWorkspace({ name: newWorkspaceName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, }) .signers([newWorkspace]) .postInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newBudgetPublicKey, lamports: - (await program.provider.connection.getMinimumBalanceForRentExemption( + (await provider.connection.getMinimumBalanceForRentExemption( 157 // collection account size )) + - (await program.provider.connection.getMinimumBalanceForRentExemption( + (await provider.connection.getMinimumBalanceForRentExemption( 9 // collection stats account size )) + - (await program.provider.connection.getMinimumBalanceForRentExemption( + (await provider.connection.getMinimumBalanceForRentExemption( 125 // application account size )) + - (await program.provider.connection.getMinimumBalanceForRentExemption( + (await provider.connection.getMinimumBalanceForRentExemption( 10 // application stats account size )), }), @@ -366,7 +371,7 @@ describe('collection attribute', () => { await program.methods .createApplication({ name: newApplicationName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, application: newApplication.publicKey, }) @@ -375,7 +380,7 @@ describe('collection attribute', () => { await program.methods .createCollection({ name: newCollectionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, application: newApplication.publicKey, collection: newCollection.publicKey, @@ -386,7 +391,7 @@ describe('collection attribute', () => { await program.methods .createCollectionAttribute(attributesData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, application: newApplication.publicKey, collection: newCollection.publicKey, @@ -395,10 +400,10 @@ describe('collection attribute', () => { .signers([newAttribute]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6027); + assert.equal(error?.error.errorCode.number, 6027); }); it('should fail when user is not a collaborator', async () => { @@ -413,7 +418,7 @@ describe('collection attribute', () => { max: null, maxLength: null, }; - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods @@ -428,17 +433,17 @@ describe('collection attribute', () => { .signers([newUser, newAttribute]) .preInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newUser.publicKey, lamports: LAMPORTS_PER_SOL, }), ]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 3012); + assert.equal(error?.error.errorCode.number, 3012); }); it('should fail when user is not an approved collaborator', async () => { @@ -453,21 +458,25 @@ describe('collection attribute', () => { maxLength: null, }; const newUser = Keypair.generate(); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act const [newUserPublicKey] = await PublicKey.findProgramAddress( [Buffer.from('user', 'utf8'), newUser.publicKey.toBuffer()], program.programId ); await program.methods - .createUser() + .createUser({ + name: newUserName, + thumbnailUrl: newUserThumbnailUrl, + userName: newUserUserName, + }) .accounts({ authority: newUser.publicKey, }) .signers([newUser]) .preInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newUser.publicKey, lamports: LAMPORTS_PER_SOL, }), @@ -496,9 +505,9 @@ describe('collection attribute', () => { .signers([newUser, newAttribute]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6029); + assert.equal(error?.error.errorCode.number, 6029); }); }); diff --git a/apps/bulldozer-programs/tests/collection.spec.ts b/apps/bulldozer-programs/tests/collection.spec.ts index 5d621ce1..31dfc463 100644 --- a/apps/bulldozer-programs/tests/collection.spec.ts +++ b/apps/bulldozer-programs/tests/collection.spec.ts @@ -1,4 +1,4 @@ -import { Program, ProgramError, Provider } from '@heavy-duty/anchor'; +import { AnchorError, AnchorProvider, Program } from '@heavy-duty/anchor'; import { Keypair, LAMPORTS_PER_SOL, @@ -10,11 +10,8 @@ import { Bulldozer, IDL } from '../target/types/bulldozer'; import { BULLDOZER_PROGRAM_ID } from './utils'; describe('collection', () => { - const program = new Program( - IDL, - BULLDOZER_PROGRAM_ID, - Provider.env() - ); + const provider = AnchorProvider.env(); + const program = new Program(IDL, BULLDOZER_PROGRAM_ID, provider); const collection = Keypair.generate(); const application = Keypair.generate(); const workspace = Keypair.generate(); @@ -22,6 +19,12 @@ describe('collection', () => { const workspaceName = 'my-workspace'; let budgetPublicKey: PublicKey; let applicationStatsPublicKey: PublicKey; + const userUserName = 'user-name-1'; + const userName = 'User Name 1'; + const userThumbnailUrl = 'https://img/1.com'; + const newUserUserName = 'user-name-2'; + const newUserName = 'User Name 2'; + const newUserThumbnailUrl = 'https://img/2.com'; before(async () => { [budgetPublicKey] = await PublicKey.findProgramAddress( @@ -38,9 +41,13 @@ describe('collection', () => { try { await program.methods - .createUser() + .createUser({ + name: userName, + thumbnailUrl: userThumbnailUrl, + userName: userUserName, + }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, }) .rpc(); } catch (error) {} @@ -48,13 +55,13 @@ describe('collection', () => { await program.methods .createWorkspace({ name: workspaceName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, }) .signers([workspace]) .postInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: budgetPublicKey, lamports: LAMPORTS_PER_SOL, }), @@ -63,7 +70,7 @@ describe('collection', () => { await program.methods .createApplication({ name: applicationName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, }) @@ -81,7 +88,7 @@ describe('collection', () => { collection: collection.publicKey, application: application.publicKey, workspace: workspace.publicKey, - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, }) .signers([collection]) .rpc(); @@ -91,7 +98,7 @@ describe('collection', () => { ); const applicationStatsAccount = await program.account.applicationStats.fetch(applicationStatsPublicKey); - assert.ok(account.authority.equals(program.provider.wallet.publicKey)); + assert.ok(account.authority.equals(provider.wallet.publicKey)); assert.ok(account.workspace.equals(workspace.publicKey)); assert.ok(account.application.equals(application.publicKey)); assert.equal(account.name, collectionName); @@ -106,7 +113,7 @@ describe('collection', () => { await program.methods .updateCollection({ name: collectionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, collection: collection.publicKey, @@ -125,7 +132,7 @@ describe('collection', () => { await program.methods .deleteCollection() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, collection: collection.publicKey, @@ -154,13 +161,13 @@ describe('collection', () => { max: null, maxLength: null, }; - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods .createCollection({ name: collectionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, collection: collection.publicKey, @@ -170,7 +177,7 @@ describe('collection', () => { await program.methods .createCollectionAttribute(argumentsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, collection: collection.publicKey, @@ -181,17 +188,17 @@ describe('collection', () => { await program.methods .deleteCollection() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, collection: collection.publicKey, }) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6013); + assert.equal(error?.error.errorCode.number, 6013); }); it('should fail when providing wrong "application" to delete', async () => { @@ -200,13 +207,13 @@ describe('collection', () => { const newApplicationName = 'sample'; const newCollection = Keypair.generate(); const newCollectionName = 'sample'; - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods .createApplication({ name: newApplicationName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: newApplication.publicKey, }) @@ -215,7 +222,7 @@ describe('collection', () => { await program.methods .createCollection({ name: newCollectionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: newApplication.publicKey, collection: newCollection.publicKey, @@ -225,17 +232,17 @@ describe('collection', () => { await program.methods .deleteCollection() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, collection: newCollection.publicKey, }) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6035); + assert.equal(error?.error.errorCode.number, 6035); }); it('should fail when workspace has insufficient funds', async () => { @@ -250,18 +257,18 @@ describe('collection', () => { [Buffer.from('budget', 'utf8'), newWorkspace.publicKey.toBuffer()], program.programId ); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act await program.methods .createWorkspace({ name: newWorkspaceName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, }) .signers([newWorkspace]) .postInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newBudgetPublicKey, lamports: (await program.provider.connection.getMinimumBalanceForRentExemption( @@ -276,7 +283,7 @@ describe('collection', () => { await program.methods .createApplication({ name: newApplicationName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, application: newApplication.publicKey, }) @@ -286,7 +293,7 @@ describe('collection', () => { await program.methods .createCollection({ name: newCollectionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, application: newApplication.publicKey, collection: newCollection.publicKey, @@ -294,10 +301,10 @@ describe('collection', () => { .signers([newCollection]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6027); + assert.equal(error?.error.errorCode.number, 6027); }); it('should fail when user is not a collaborator', async () => { @@ -305,7 +312,7 @@ describe('collection', () => { const newCollection = Keypair.generate(); const newCollectionName = 'sample'; const newUser = Keypair.generate(); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods @@ -319,17 +326,17 @@ describe('collection', () => { .signers([newUser, newCollection]) .preInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newUser.publicKey, lamports: LAMPORTS_PER_SOL, }), ]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 3012); + assert.equal(error?.error.errorCode.number, 3012); }); it('should fail when user is not an approved collaborator', async () => { @@ -337,21 +344,25 @@ describe('collection', () => { const newCollection = Keypair.generate(); const newCollectionName = 'sample'; const newUser = Keypair.generate(); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act const [newUserPublicKey] = await PublicKey.findProgramAddress( [Buffer.from('user', 'utf8'), newUser.publicKey.toBuffer()], program.programId ); await program.methods - .createUser() + .createUser({ + name: newUserName, + thumbnailUrl: newUserThumbnailUrl, + userName: newUserUserName, + }) .accounts({ authority: newUser.publicKey, }) .signers([newUser]) .preInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newUser.publicKey, lamports: LAMPORTS_PER_SOL, }), @@ -379,9 +390,9 @@ describe('collection', () => { .signers([newUser, newCollection]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6029); + assert.equal(error?.error.errorCode.number, 6029); }); }); diff --git a/apps/bulldozer-programs/tests/instruction-account.spec.ts b/apps/bulldozer-programs/tests/instruction-account.spec.ts index 87300031..be466b98 100644 --- a/apps/bulldozer-programs/tests/instruction-account.spec.ts +++ b/apps/bulldozer-programs/tests/instruction-account.spec.ts @@ -1,4 +1,4 @@ -import { Program, ProgramError, Provider } from '@heavy-duty/anchor'; +import { AnchorError, AnchorProvider, Program } from '@heavy-duty/anchor'; import { Keypair, LAMPORTS_PER_SOL, @@ -14,11 +14,8 @@ import { } from './utils'; describe('instruction account', () => { - const program = new Program( - IDL, - BULLDOZER_PROGRAM_ID, - Provider.env() - ); + const provider = AnchorProvider.env(); + const program = new Program(IDL, BULLDOZER_PROGRAM_ID, provider); const instruction = Keypair.generate(); const instructionName = 'create_document'; const collection = Keypair.generate(); @@ -31,6 +28,12 @@ describe('instruction account', () => { const workspaceName = 'my-workspace'; let budgetPublicKey: PublicKey; let instructionStatsPublicKey: PublicKey; + const userUserName = 'user-name-1'; + const userName = 'User Name 1'; + const userThumbnailUrl = 'https://img/1.com'; + const newUserUserName = 'user-name-2'; + const newUserName = 'User Name 2'; + const newUserThumbnailUrl = 'https://img/2.com'; before(async () => { [budgetPublicKey] = await PublicKey.findProgramAddress( @@ -47,9 +50,13 @@ describe('instruction account', () => { try { await program.methods - .createUser() + .createUser({ + name: userName, + thumbnailUrl: userThumbnailUrl, + userName: userUserName, + }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, }) .rpc(); } catch (error) {} @@ -57,13 +64,13 @@ describe('instruction account', () => { await program.methods .createWorkspace({ name: workspaceName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, }) .signers([workspace]) .postInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: budgetPublicKey, lamports: LAMPORTS_PER_SOL, }), @@ -72,7 +79,7 @@ describe('instruction account', () => { await program.methods .createApplication({ name: applicationName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, }) @@ -81,7 +88,7 @@ describe('instruction account', () => { await program.methods .createInstruction({ name: instructionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -94,7 +101,7 @@ describe('instruction account', () => { collection: collection.publicKey, workspace: workspace.publicKey, application: application.publicKey, - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, }) .signers([collection]) .rpc(); @@ -104,7 +111,7 @@ describe('instruction account', () => { collection: anotherCollection.publicKey, workspace: workspace.publicKey, application: application.publicKey, - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, }) .signers([anotherCollection]) .rpc(); @@ -113,36 +120,6 @@ describe('instruction account', () => { describe('document', () => { const instructionAccount = Keypair.generate(); - it('should fail when creating without collection', async () => { - // arrange - const instructionAccount = Keypair.generate(); - const accountsData = { - name: '12345678901234567890123456789012', - kind: 0, - modifier: null, - space: null, - }; - let error: ProgramError | null = null; - // act - try { - await program.methods - .createInstructionAccount(accountsData) - .accounts({ - authority: program.provider.wallet.publicKey, - workspace: workspace.publicKey, - application: application.publicKey, - instruction: instruction.publicKey, - account: instructionAccount.publicKey, - }) - .signers([instructionAccount]) - .rpc(); - } catch (err) { - error = err as ProgramError; - } - // assert - assert.equal(error?.code, 6005); - }); - it('should create', async () => { // arrange const accountsData = { @@ -152,71 +129,63 @@ describe('instruction account', () => { space: null, }; // act - const signature = await program.methods + await program.methods .createInstructionAccount(accountsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, account: instructionAccount.publicKey, }) .signers([instructionAccount]) - .remainingAccounts([ - { - pubkey: collection.publicKey, - isWritable: false, - isSigner: false, - }, + .postInstructions([ + await program.methods + .setInstructionAccountCollection() + .accounts({ + authority: provider.wallet.publicKey, + workspace: workspace.publicKey, + application: application.publicKey, + instruction: instruction.publicKey, + collection: collection.publicKey, + account: instructionAccount.publicKey, + }) + .instruction(), ]) .rpc(); - await program.provider.connection.confirmTransaction(signature); // assert const account = await program.account.instructionAccount.fetch( instructionAccount.publicKey ); + const instructionAccountCollectionPublicKey = + await PublicKey.createProgramAddress( + [ + Buffer.from('instruction_account_collection', 'utf8'), + instructionAccount.publicKey.toBuffer(), + Buffer.from([account.bumps.collection]), + ], + program.programId + ); + const instructionAccountCollection = + await program.account.instructionAccountCollection.fetch( + instructionAccountCollectionPublicKey + ); const instructionStatsAccount = await program.account.instructionStats.fetch(instructionStatsPublicKey); const decodedKind = decodeAccountKind(account.kind as any); - assert.ok(account.authority.equals(program.provider.wallet.publicKey)); + assert.ok(account.authority.equals(provider.wallet.publicKey)); assert.ok(account.instruction.equals(instruction.publicKey)); assert.ok(account.workspace.equals(workspace.publicKey)); assert.ok(account.application.equals(application.publicKey)); assert.equal(account.name, accountsData.name); assert.ok('document' in account.kind); assert.equal(decodedKind.id, accountsData.kind); - assert.equal(decodedKind.collection, collection.publicKey.toBase58()); assert.equal(account.modifier, null); assert.ok(account.createdAt.eq(account.updatedAt)); assert.equal(instructionStatsAccount.quantityOfAccounts, 1); - }); - - it('should remove collection when changing the kind', async () => { - // arrange - const accountsData = { - name: 'data', - kind: 1, - modifier: null, - space: null, - }; - // act - await program.methods - .updateInstructionAccount(accountsData) - .accounts({ - authority: program.provider.wallet.publicKey, - workspace: workspace.publicKey, - instruction: instruction.publicKey, - account: instructionAccount.publicKey, - }) - .rpc(); - // assert - const account = await program.account.instructionAccount.fetch( - instructionAccount.publicKey + assert.ok( + instructionAccountCollection.collection?.equals(collection.publicKey) ); - const decodedKind = decodeAccountKind(account.kind as any); - assert.ok('signer' in account.kind); - assert.equal(decodedKind.id, accountsData.kind); - assert.ok(account.createdAt.lte(account.updatedAt)); }); it('should delete', async () => { @@ -232,25 +201,18 @@ describe('instruction account', () => { await program.methods .createInstructionAccount(accountsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, account: instructionAccount.publicKey, }) .signers([instructionAccount]) - .remainingAccounts([ - { - pubkey: collection.publicKey, - isWritable: false, - isSigner: false, - }, - ]) .rpc(); await program.methods .deleteInstructionAccount() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, account: instructionAccount.publicKey, instruction: instruction.publicKey, @@ -278,7 +240,7 @@ describe('instruction account', () => { await program.methods .createInstructionAccount(accountsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -300,38 +262,63 @@ describe('instruction account', () => { await program.methods .createInstructionAccount(accountsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, account: instructionAccount.publicKey, }) .signers([instructionAccount]) - .remainingAccounts([ - { - pubkey: collection.publicKey, - isWritable: false, - isSigner: false, - }, - { - pubkey: instructionPayerAccount.publicKey, - isWritable: false, - isSigner: false, - }, + .postInstructions([ + await program.methods + .setInstructionAccountCollection() + .accounts({ + authority: provider.wallet.publicKey, + workspace: workspace.publicKey, + application: application.publicKey, + instruction: instruction.publicKey, + collection: collection.publicKey, + account: instructionAccount.publicKey, + }) + .instruction(), + await program.methods + .setInstructionAccountPayer() + .accounts({ + authority: provider.wallet.publicKey, + workspace: workspace.publicKey, + instruction: instruction.publicKey, + payer: instructionPayerAccount.publicKey, + account: instructionAccount.publicKey, + }) + .instruction(), ]) .rpc(); // assert const account = await program.account.instructionAccount.fetch( instructionAccount.publicKey ); + const instructionAccountPayerPublicKey = + await PublicKey.createProgramAddress( + [ + Buffer.from('instruction_account_payer', 'utf8'), + instructionAccount.publicKey.toBuffer(), + Buffer.from([account.bumps.payer]), + ], + program.programId + ); + const instructionAccountPayer = + await program.account.instructionAccountPayer.fetch( + instructionAccountPayerPublicKey + ); const decodedModifier = decodeAccountModifier(account.modifier as any); assert.equal(decodedModifier.id, accountsData.modifier); assert.equal(decodedModifier.name, 'init'); - assert.equal( - decodedModifier.payer, - instructionPayerAccount.publicKey.toBase58() + assert.equal(account.space, 150); + assert.ok( + instructionAccountPayer.payer?.equals( + instructionPayerAccount.publicKey + ) ); - assert.equal(decodedModifier.space, 150); }); it('should remove payer and space when changing the modifier', async () => { @@ -346,26 +333,32 @@ describe('instruction account', () => { await program.methods .updateInstructionAccount(accountsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, instruction: instruction.publicKey, account: instructionAccount.publicKey, }) - .remainingAccounts([ - { - pubkey: collection.publicKey, - isWritable: false, - isSigner: false, - }, - ]) .rpc(); // assert const account = await program.account.instructionAccount.fetch( instructionAccount.publicKey ); + const instructionAccountPayerPublicKey = + await PublicKey.createProgramAddress( + [ + Buffer.from('instruction_account_payer', 'utf8'), + instructionAccount.publicKey.toBuffer(), + Buffer.from([account.bumps.payer]), + ], + program.programId + ); + const instructionAccountPayer = + await program.account.instructionAccountPayer.fetch( + instructionAccountPayerPublicKey + ); assert.equal(account.modifier, null); - assert.equal(account.payer, null); assert.equal(account.space, null); + assert.equal(instructionAccountPayer.payer, null); }); it('should fail when space is not provided', async () => { @@ -377,96 +370,35 @@ describe('instruction account', () => { modifier: 0, space: null, }; - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods .createInstructionAccount(accountsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, account: instructionAccount.publicKey, }) .signers([instructionAccount]) - .remainingAccounts([ - { - pubkey: collection.publicKey, - isWritable: false, - isSigner: false, - }, - { - pubkey: instructionPayerAccount.publicKey, - isWritable: false, - isSigner: false, - }, - ]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6007); + assert.equal(error?.error.errorCode.number, 6007); }); }); describe('with mut modifier', () => { const instructionAccount = Keypair.generate(); - - it('should create', async () => { - // arrange - const accountsData = { - name: 'data', - kind: 0, - modifier: 1, - space: null, - }; - // act - await program.methods - .createInstructionAccount(accountsData) - .accounts({ - authority: program.provider.wallet.publicKey, - workspace: workspace.publicKey, - application: application.publicKey, - instruction: instruction.publicKey, - account: instructionAccount.publicKey, - }) - .signers([instructionAccount]) - .remainingAccounts([ - { - pubkey: collection.publicKey, - isWritable: false, - isSigner: false, - }, - ]) - .rpc(); - // assert - const account = await program.account.instructionAccount.fetch( - instructionAccount.publicKey - ); - const decodedKind = decodeAccountKind(account.kind as any); - const decodedModifier = decodeAccountModifier(account.modifier as any); - assert.ok(account.authority.equals(program.provider.wallet.publicKey)); - assert.ok(account.instruction.equals(instruction.publicKey)); - assert.ok(account.workspace.equals(workspace.publicKey)); - assert.ok(account.application.equals(application.publicKey)); - assert.equal(account.name, accountsData.name); - assert.equal(decodedKind.id, accountsData.kind); - assert.equal(decodedKind.collection, collection.publicKey.toBase58()); - assert.equal(decodedModifier.id, accountsData.modifier); - assert.equal(decodedModifier.name, 'mut'); - assert.equal(decodedModifier.close, null); - }); - }); - - describe('with mut modifier and close constraint', () => { - const instructionAccount = Keypair.generate(); const instructionCloseAccount = Keypair.generate(); before(async () => { const accountsData = { - name: 'payer', + name: 'close', kind: 1, modifier: 1, space: null, @@ -475,7 +407,7 @@ describe('instruction account', () => { await program.methods .createInstructionAccount(accountsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -497,45 +429,68 @@ describe('instruction account', () => { await program.methods .createInstructionAccount(accountsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, account: instructionAccount.publicKey, }) .signers([instructionAccount]) - .remainingAccounts([ - { - pubkey: collection.publicKey, - isWritable: false, - isSigner: false, - }, - { - pubkey: instructionCloseAccount.publicKey, - isWritable: false, - isSigner: false, - }, + .postInstructions([ + await program.methods + .setInstructionAccountCollection() + .accounts({ + authority: provider.wallet.publicKey, + workspace: workspace.publicKey, + application: application.publicKey, + instruction: instruction.publicKey, + collection: collection.publicKey, + account: instructionAccount.publicKey, + }) + .instruction(), + await program.methods + .setInstructionAccountClose() + .accounts({ + authority: provider.wallet.publicKey, + workspace: workspace.publicKey, + instruction: instruction.publicKey, + close: instructionCloseAccount.publicKey, + account: instructionAccount.publicKey, + }) + .instruction(), ]) .rpc(); // assert const account = await program.account.instructionAccount.fetch( instructionAccount.publicKey ); + const instructionAccountClosePublicKey = + await PublicKey.createProgramAddress( + [ + Buffer.from('instruction_account_close', 'utf8'), + instructionAccount.publicKey.toBuffer(), + Buffer.from([account.bumps.close]), + ], + program.programId + ); + const instructionAccountClose = + await program.account.instructionAccountClose.fetch( + instructionAccountClosePublicKey + ); const decodedKind = decodeAccountKind(account.kind as any); const decodedModifier = decodeAccountModifier(account.modifier as any); - assert.ok(account.authority.equals(program.provider.wallet.publicKey)); + assert.ok(account.authority.equals(provider.wallet.publicKey)); assert.ok(account.instruction.equals(instruction.publicKey)); assert.ok(account.workspace.equals(workspace.publicKey)); assert.ok(account.application.equals(application.publicKey)); assert.equal(account.name, accountsData.name); assert.equal(decodedKind.id, accountsData.kind); - assert.equal(decodedKind.name, 'document'); - assert.equal(decodedKind.collection, collection.publicKey.toBase58()); assert.equal(decodedModifier.id, accountsData.modifier); assert.equal(decodedModifier.name, 'mut'); - assert.equal( - decodedModifier.close, - instructionCloseAccount.publicKey.toBase58() + assert.ok( + instructionAccountClose.close?.equals( + instructionCloseAccount.publicKey + ) ); }); @@ -543,7 +498,7 @@ describe('instruction account', () => { // arrange const accountsData = { name: 'data', - kind: 1, + kind: 0, modifier: null, space: null, }; @@ -551,7 +506,7 @@ describe('instruction account', () => { await program.methods .updateInstructionAccount(accountsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, instruction: instruction.publicKey, account: instructionAccount.publicKey, @@ -561,12 +516,81 @@ describe('instruction account', () => { const account = await program.account.instructionAccount.fetch( instructionAccount.publicKey ); - const decodedKind = decodeAccountKind(account.kind as any); - assert.equal(decodedKind.id, accountsData.kind); - assert.equal(decodedKind.name, 'signer'); + const instructionAccountClosePublicKey = + await PublicKey.createProgramAddress( + [ + Buffer.from('instruction_account_close', 'utf8'), + instructionAccount.publicKey.toBuffer(), + Buffer.from([account.bumps.close]), + ], + program.programId + ); + const instructionAccountClose = + await program.account.instructionAccountClose.fetch( + instructionAccountClosePublicKey + ); assert.equal(account.modifier, null); - assert.equal(account.close, null); assert.equal(account.space, null); + assert.equal(instructionAccountClose.close, null); + }); + + it('should remove close when clearing', async () => { + // arrange + const accountsData = { + name: 'data', + kind: 0, + modifier: 1, + space: null, + }; + // act + await program.methods + .updateInstructionAccount(accountsData) + .accounts({ + authority: provider.wallet.publicKey, + workspace: workspace.publicKey, + instruction: instruction.publicKey, + account: instructionAccount.publicKey, + }) + .postInstructions([ + await program.methods + .setInstructionAccountClose() + .accounts({ + authority: provider.wallet.publicKey, + workspace: workspace.publicKey, + instruction: instruction.publicKey, + close: instructionCloseAccount.publicKey, + account: instructionAccount.publicKey, + }) + .instruction(), + ]) + .rpc(); + await program.methods + .clearInstructionAccountClose() + .accounts({ + authority: provider.wallet.publicKey, + workspace: workspace.publicKey, + instruction: instruction.publicKey, + account: instructionAccount.publicKey, + }) + .rpc(); + // assert + const account = await program.account.instructionAccount.fetch( + instructionAccount.publicKey + ); + const instructionAccountClosePublicKey = + await PublicKey.createProgramAddress( + [ + Buffer.from('instruction_account_close', 'utf8'), + instructionAccount.publicKey.toBuffer(), + Buffer.from([account.bumps.close]), + ], + program.programId + ); + const instructionAccountClose = + await program.account.instructionAccountClose.fetch( + instructionAccountClosePublicKey + ); + assert.equal(instructionAccountClose.close, null); }); }); }); @@ -586,7 +610,7 @@ describe('instruction account', () => { await program.methods .createInstructionAccount(accountsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -599,7 +623,7 @@ describe('instruction account', () => { instructionAccount.publicKey ); const decodedKind = decodeAccountKind(account.kind as any); - assert.ok(account.authority.equals(program.provider.wallet.publicKey)); + assert.ok(account.authority.equals(provider.wallet.publicKey)); assert.ok(account.instruction.equals(instruction.publicKey)); assert.ok(account.workspace.equals(workspace.publicKey)); assert.ok(account.application.equals(application.publicKey)); @@ -607,9 +631,6 @@ describe('instruction account', () => { assert.equal(decodedKind.id, accountsData.kind); assert.equal(decodedKind.name, 'signer'); assert.equal(account.modifier, null); - assert.equal(account.collection, null); - assert.equal(account.payer, null); - assert.equal(account.close, null); assert.equal(account.space, null); }); }); @@ -629,7 +650,7 @@ describe('instruction account', () => { await program.methods .createInstruction({ name: instructionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -648,13 +669,13 @@ describe('instruction account', () => { modifier: null, space: null, }; - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods .createInstructionAccount(accountsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -672,25 +693,18 @@ describe('instruction account', () => { await program.methods .createInstructionAccount(accountsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, account: instructionAccount2.publicKey, }) .signers([instructionAccount2]) - .remainingAccounts([ - { - pubkey: collection.publicKey, - isWritable: false, - isSigner: false, - }, - ]) .rpc(); await program.methods .createInstructionRelation() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -701,17 +715,17 @@ describe('instruction account', () => { await program.methods .deleteInstructionAccount() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, instruction: instruction.publicKey, account: instructionAccount1.publicKey, }) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6015); + assert.equal(error?.error.errorCode.number, 6015); }); it('should increment instruction account quantity on create', async () => { @@ -735,7 +749,7 @@ describe('instruction account', () => { await program.methods .createInstruction({ name: instructionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -745,7 +759,7 @@ describe('instruction account', () => { await program.methods .createInstructionAccount(accountsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -787,7 +801,7 @@ describe('instruction account', () => { await program.methods .createInstruction({ name: instructionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -797,25 +811,18 @@ describe('instruction account', () => { await program.methods .createInstructionAccount(accountsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, account: instructionAccount.publicKey, }) .signers([instructionAccount]) - .remainingAccounts([ - { - pubkey: collection.publicKey, - isWritable: false, - isSigner: false, - }, - ]) .rpc(); await program.methods .deleteInstructionAccount() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, instruction: instruction.publicKey, account: instructionAccount.publicKey, @@ -838,13 +845,13 @@ describe('instruction account', () => { modifier: null, space: null, }; - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods .createInstruction({ name: newInstructionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: newInstruction.publicKey, @@ -854,7 +861,7 @@ describe('instruction account', () => { await program.methods .createInstructionAccount(accountsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: newInstruction.publicKey, @@ -865,17 +872,17 @@ describe('instruction account', () => { await program.methods .deleteInstructionAccount() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, instruction: instruction.publicKey, account: newAccount.publicKey, }) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6044); + assert.equal(error?.error.errorCode.number, 6044); }); it('should fail when workspace has insufficient funds', async () => { @@ -897,30 +904,30 @@ describe('instruction account', () => { [Buffer.from('budget', 'utf8'), newWorkspace.publicKey.toBuffer()], program.programId ); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act await program.methods .createWorkspace({ name: newWorkspaceName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, }) .signers([newWorkspace]) .postInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newBudgetPublicKey, lamports: - (await program.provider.connection.getMinimumBalanceForRentExemption( + (await provider.connection.getMinimumBalanceForRentExemption( 2155 // instruction account size )) + - (await program.provider.connection.getMinimumBalanceForRentExemption( + (await provider.connection.getMinimumBalanceForRentExemption( 10 // instruction stats account size )) + - (await program.provider.connection.getMinimumBalanceForRentExemption( + (await provider.connection.getMinimumBalanceForRentExemption( 125 // application account size )) + - (await program.provider.connection.getMinimumBalanceForRentExemption( + (await provider.connection.getMinimumBalanceForRentExemption( 10 // application stats account size )), }), @@ -929,7 +936,7 @@ describe('instruction account', () => { await program.methods .createApplication({ name: newApplicationName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, application: newApplication.publicKey, }) @@ -938,7 +945,7 @@ describe('instruction account', () => { await program.methods .createInstruction({ name: newInstructionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, application: newApplication.publicKey, instruction: newInstruction.publicKey, @@ -949,26 +956,19 @@ describe('instruction account', () => { await program.methods .createInstructionAccount(accountsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, application: newApplication.publicKey, instruction: newInstruction.publicKey, account: newAccount.publicKey, }) .signers([newAccount]) - .remainingAccounts([ - { - pubkey: collection.publicKey, - isWritable: false, - isSigner: false, - }, - ]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6027); + assert.equal(error?.error.errorCode.number, 6027); }); it('should fail when user is not a collaborator', async () => { @@ -981,7 +981,7 @@ describe('instruction account', () => { modifier: null, space: null, }; - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods @@ -996,17 +996,17 @@ describe('instruction account', () => { .signers([newUser, newAccount]) .preInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newUser.publicKey, lamports: LAMPORTS_PER_SOL, }), ]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 3012); + assert.equal(error?.error.errorCode.number, 3012); }); it('should fail when user is not an approved collaborator', async () => { @@ -1019,21 +1019,25 @@ describe('instruction account', () => { space: null, }; const newUser = Keypair.generate(); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act const [newUserPublicKey] = await PublicKey.findProgramAddress( [Buffer.from('user', 'utf8'), newUser.publicKey.toBuffer()], program.programId ); await program.methods - .createUser() + .createUser({ + name: newUserName, + thumbnailUrl: newUserThumbnailUrl, + userName: newUserUserName, + }) .accounts({ authority: newUser.publicKey, }) .signers([newUser]) .preInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newUser.publicKey, lamports: LAMPORTS_PER_SOL, }), @@ -1062,10 +1066,10 @@ describe('instruction account', () => { .signers([newUser, newAccount]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6029); + assert.equal(error?.error.errorCode.number, 6029); }); }); }); diff --git a/apps/bulldozer-programs/tests/instruction-argument.spec.ts b/apps/bulldozer-programs/tests/instruction-argument.spec.ts index a4ea22f2..db83d450 100644 --- a/apps/bulldozer-programs/tests/instruction-argument.spec.ts +++ b/apps/bulldozer-programs/tests/instruction-argument.spec.ts @@ -1,4 +1,4 @@ -import { Program, ProgramError, Provider } from '@heavy-duty/anchor'; +import { AnchorError, AnchorProvider, Program } from '@heavy-duty/anchor'; import { Keypair, LAMPORTS_PER_SOL, @@ -10,11 +10,8 @@ import { Bulldozer, IDL } from '../target/types/bulldozer'; import { BULLDOZER_PROGRAM_ID, decodeAttributeEnum } from './utils'; describe('instruction argument', () => { - const program = new Program( - IDL, - BULLDOZER_PROGRAM_ID, - Provider.env() - ); + const provider = AnchorProvider.env(); + const program = new Program(IDL, BULLDOZER_PROGRAM_ID, provider); const instruction = Keypair.generate(); const instructionArgument = Keypair.generate(); const instructionName = 'create_document'; @@ -24,6 +21,12 @@ describe('instruction argument', () => { const workspaceName = 'my-workspace'; let budgetPublicKey: PublicKey; let instructionStatsPublicKey: PublicKey; + const userUserName = 'user-name-1'; + const userName = 'User Name 1'; + const userThumbnailUrl = 'https://img/1.com'; + const newUserUserName = 'user-name-2'; + const newUserName = 'User Name 2'; + const newUserThumbnailUrl = 'https://img/2.com'; before(async () => { [budgetPublicKey] = await PublicKey.findProgramAddress( @@ -40,9 +43,13 @@ describe('instruction argument', () => { try { await program.methods - .createUser() + .createUser({ + name: userName, + thumbnailUrl: userThumbnailUrl, + userName: userUserName, + }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, }) .rpc(); } catch (error) {} @@ -50,13 +57,13 @@ describe('instruction argument', () => { await program.methods .createWorkspace({ name: workspaceName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, }) .signers([workspace]) .postInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: budgetPublicKey, lamports: LAMPORTS_PER_SOL, }), @@ -65,7 +72,7 @@ describe('instruction argument', () => { await program.methods .createApplication({ name: applicationName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, }) @@ -74,7 +81,7 @@ describe('instruction argument', () => { await program.methods .createInstruction({ name: instructionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -97,7 +104,7 @@ describe('instruction argument', () => { await program.methods .createInstructionArgument(argumentsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -116,9 +123,7 @@ describe('instruction argument', () => { instructionArgumentAccount.kind as any ); assert.ok( - instructionArgumentAccount.authority.equals( - program.provider.wallet.publicKey - ) + instructionArgumentAccount.authority.equals(provider.wallet.publicKey) ); assert.ok(instructionArgumentAccount.workspace.equals(workspace.publicKey)); assert.ok( @@ -154,7 +159,7 @@ describe('instruction argument', () => { await program.methods .updateInstructionArgument(argumentsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, instruction: instruction.publicKey, argument: instructionArgument.publicKey, @@ -181,7 +186,7 @@ describe('instruction argument', () => { await program.methods .deleteInstructionArgument() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, argument: instructionArgument.publicKey, instruction: instruction.publicKey, @@ -208,13 +213,13 @@ describe('instruction argument', () => { max: null, maxLength: null, }; - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods .createInstructionArgument(argumentsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -223,10 +228,10 @@ describe('instruction argument', () => { .signers([instructionArgument]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6011); + assert.equal(error?.error.errorCode.number, 6011); }); it('should fail when max length is not provided with a string', async () => { @@ -239,13 +244,13 @@ describe('instruction argument', () => { max: null, maxLength: null, }; - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods .createInstructionArgument(argumentsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -254,10 +259,10 @@ describe('instruction argument', () => { .signers([instructionArgument]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6012); + assert.equal(error?.error.errorCode.number, 6012); }); it('should fail when providing wrong "instruction" to delete', async () => { @@ -273,13 +278,13 @@ describe('instruction argument', () => { max: null, maxLength: null, }; - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods .createInstruction({ name: newInstructionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: newInstruction.publicKey, @@ -289,7 +294,7 @@ describe('instruction argument', () => { await program.methods .createInstructionArgument(argumentsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: newInstruction.publicKey, @@ -300,17 +305,17 @@ describe('instruction argument', () => { await program.methods .deleteInstructionArgument() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, instruction: instruction.publicKey, argument: newArgument.publicKey, }) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6041); + assert.equal(error?.error.errorCode.number, 6041); }); it('should fail when workspace has insufficient funds', async () => { @@ -334,30 +339,30 @@ describe('instruction argument', () => { [Buffer.from('budget', 'utf8'), newWorkspace.publicKey.toBuffer()], program.programId ); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act await program.methods .createWorkspace({ name: newWorkspaceName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, }) .signers([newWorkspace]) .postInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newBudgetPublicKey, lamports: - (await program.provider.connection.getMinimumBalanceForRentExemption( + (await provider.connection.getMinimumBalanceForRentExemption( 2155 // instruction account size )) + - (await program.provider.connection.getMinimumBalanceForRentExemption( + (await provider.connection.getMinimumBalanceForRentExemption( 10 // instruction stats account size )) + - (await program.provider.connection.getMinimumBalanceForRentExemption( + (await provider.connection.getMinimumBalanceForRentExemption( 125 // application account size )) + - (await program.provider.connection.getMinimumBalanceForRentExemption( + (await provider.connection.getMinimumBalanceForRentExemption( 10 // application stats account size )), }), @@ -366,7 +371,7 @@ describe('instruction argument', () => { await program.methods .createApplication({ name: newApplicationName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, application: newApplication.publicKey, }) @@ -375,7 +380,7 @@ describe('instruction argument', () => { await program.methods .createInstruction({ name: newInstructionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, application: newApplication.publicKey, instruction: newInstruction.publicKey, @@ -386,7 +391,7 @@ describe('instruction argument', () => { await program.methods .createInstructionArgument(argumentsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, application: newApplication.publicKey, instruction: newInstruction.publicKey, @@ -395,10 +400,10 @@ describe('instruction argument', () => { .signers([newArgument]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6027); + assert.equal(error?.error.errorCode.number, 6027); }); it('should fail when user is not a collaborator', async () => { @@ -413,7 +418,7 @@ describe('instruction argument', () => { max: null, maxLength: null, }; - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods @@ -428,17 +433,17 @@ describe('instruction argument', () => { .signers([newUser, newArgument]) .preInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newUser.publicKey, lamports: LAMPORTS_PER_SOL, }), ]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 3012); + assert.equal(error?.error.errorCode.number, 3012); }); it('should fail when user is not an approved collaborator', async () => { @@ -453,21 +458,25 @@ describe('instruction argument', () => { maxLength: null, }; const newUser = Keypair.generate(); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act const [newUserPublicKey] = await PublicKey.findProgramAddress( [Buffer.from('user', 'utf8'), newUser.publicKey.toBuffer()], program.programId ); await program.methods - .createUser() + .createUser({ + name: newUserName, + thumbnailUrl: newUserThumbnailUrl, + userName: newUserUserName, + }) .accounts({ authority: newUser.publicKey, }) .signers([newUser]) .preInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newUser.publicKey, lamports: LAMPORTS_PER_SOL, }), @@ -496,9 +505,9 @@ describe('instruction argument', () => { .signers([newUser, newArgument]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6029); + assert.equal(error?.error.errorCode.number, 6029); }); }); diff --git a/apps/bulldozer-programs/tests/instruction-relation.spec.ts b/apps/bulldozer-programs/tests/instruction-relation.spec.ts index a7fb971e..f14ba322 100644 --- a/apps/bulldozer-programs/tests/instruction-relation.spec.ts +++ b/apps/bulldozer-programs/tests/instruction-relation.spec.ts @@ -1,4 +1,4 @@ -import { Program, ProgramError, Provider } from '@heavy-duty/anchor'; +import { AnchorError, AnchorProvider, Program } from '@heavy-duty/anchor'; import { Keypair, LAMPORTS_PER_SOL, @@ -10,11 +10,8 @@ import { Bulldozer, IDL } from '../target/types/bulldozer'; import { BULLDOZER_PROGRAM_ID } from './utils'; describe('instruction relation', () => { - const program = new Program( - IDL, - BULLDOZER_PROGRAM_ID, - Provider.env() - ); + const provider = AnchorProvider.env(); + const program = new Program(IDL, BULLDOZER_PROGRAM_ID, provider); const instruction = Keypair.generate(); const instructionName = 'create_document'; const application = Keypair.generate(); @@ -39,6 +36,12 @@ describe('instruction relation', () => { let budgetPublicKey: PublicKey; let fromStatsPublicKey: PublicKey; let toStatsPublicKey: PublicKey; + const userUserName = 'user-name-1'; + const userName = 'User Name 1'; + const userThumbnailUrl = 'https://img/1.com'; + const newUserUserName = 'user-name-2'; + const newUserName = 'User Name 2'; + const newUserThumbnailUrl = 'https://img/2.com'; before(async () => { [budgetPublicKey] = await PublicKey.findProgramAddress( @@ -70,9 +73,13 @@ describe('instruction relation', () => { try { await program.methods - .createUser() + .createUser({ + name: userName, + thumbnailUrl: userThumbnailUrl, + userName: userUserName, + }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, }) .rpc(); } catch (error) {} @@ -80,13 +87,13 @@ describe('instruction relation', () => { await program.methods .createWorkspace({ name: workspaceName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, }) .signers([workspace]) .postInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: budgetPublicKey, lamports: LAMPORTS_PER_SOL, }), @@ -95,7 +102,7 @@ describe('instruction relation', () => { await program.methods .createApplication({ name: applicationName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, }) @@ -104,7 +111,7 @@ describe('instruction relation', () => { await program.methods .createInstruction({ name: instructionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -114,7 +121,7 @@ describe('instruction relation', () => { await program.methods .createInstructionAccount(fromDto) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -125,7 +132,7 @@ describe('instruction relation', () => { await program.methods .createInstructionAccount(toDto) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -140,7 +147,7 @@ describe('instruction relation', () => { await program.methods .createInstructionRelation() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -157,9 +164,7 @@ describe('instruction relation', () => { toStatsPublicKey ); assert.ok( - instructionRelationAccount.authority.equals( - program.provider.wallet.publicKey - ) + instructionRelationAccount.authority.equals(provider.wallet.publicKey) ); assert.ok( instructionRelationAccount.instruction.equals(instruction.publicKey) @@ -186,7 +191,7 @@ describe('instruction relation', () => { await program.methods .createInstructionAccount(fromDto) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -197,7 +202,7 @@ describe('instruction relation', () => { await program.methods .createInstructionAccount(toDto) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -216,7 +221,7 @@ describe('instruction relation', () => { await program.methods .createInstructionRelation() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -227,7 +232,7 @@ describe('instruction relation', () => { await program.methods .deleteInstructionRelation() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, instruction: instruction.publicKey, from: newFrom.publicKey, @@ -243,13 +248,13 @@ describe('instruction relation', () => { }); it('should fail if from and to are equal', async () => { - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods .createInstructionRelation() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -258,10 +263,10 @@ describe('instruction relation', () => { }) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 2003); + assert.equal(error?.error.errorCode.number, 2003); }); it('should fail when workspace has insufficient funds', async () => { @@ -278,43 +283,61 @@ describe('instruction relation', () => { [Buffer.from('budget', 'utf8'), newWorkspace.publicKey.toBuffer()], program.programId ); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act await program.methods .createWorkspace({ name: newWorkspaceName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, }) .signers([newWorkspace]) .postInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newBudgetPublicKey, lamports: - (await program.provider.connection.getMinimumBalanceForRentExemption( + (await provider.connection.getMinimumBalanceForRentExemption( 2155 // instruction account size )) + - (await program.provider.connection.getMinimumBalanceForRentExemption( + (await provider.connection.getMinimumBalanceForRentExemption( 10 // instruction stats account size )) + - (await program.provider.connection.getMinimumBalanceForRentExemption( + (await provider.connection.getMinimumBalanceForRentExemption( 125 // application account size )) + - (await program.provider.connection.getMinimumBalanceForRentExemption( + (await provider.connection.getMinimumBalanceForRentExemption( 10 // application stats account size )) + - (await program.provider.connection.getMinimumBalanceForRentExemption( - 295 // from account size + (await provider.connection.getMinimumBalanceForRentExemption( + 264 // from account size )) + - (await program.provider.connection.getMinimumBalanceForRentExemption( + (await provider.connection.getMinimumBalanceForRentExemption( 10 // from stats account size )) + - (await program.provider.connection.getMinimumBalanceForRentExemption( - 295 // to account size + (await provider.connection.getMinimumBalanceForRentExemption( + 41 // from collection account size + )) + + (await provider.connection.getMinimumBalanceForRentExemption( + 41 // from close account size + )) + + (await provider.connection.getMinimumBalanceForRentExemption( + 41 // from payer account size + )) + + (await provider.connection.getMinimumBalanceForRentExemption( + 264 // to account size )) + - (await program.provider.connection.getMinimumBalanceForRentExemption( + (await provider.connection.getMinimumBalanceForRentExemption( 10 // to stats account size + )) + + (await provider.connection.getMinimumBalanceForRentExemption( + 41 // from collection account size + )) + + (await provider.connection.getMinimumBalanceForRentExemption( + 41 // from close account size + )) + + (await provider.connection.getMinimumBalanceForRentExemption( + 41 // from payer account size )), }), ]) @@ -322,7 +345,7 @@ describe('instruction relation', () => { await program.methods .createApplication({ name: newApplicationName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, application: newApplication.publicKey, }) @@ -331,7 +354,7 @@ describe('instruction relation', () => { await program.methods .createInstruction({ name: newInstructionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, application: newApplication.publicKey, instruction: newInstruction.publicKey, @@ -341,7 +364,7 @@ describe('instruction relation', () => { await program.methods .createInstructionAccount(fromDto) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, application: newApplication.publicKey, instruction: newInstruction.publicKey, @@ -352,7 +375,7 @@ describe('instruction relation', () => { await program.methods .createInstructionAccount(toDto) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, application: newApplication.publicKey, instruction: newInstruction.publicKey, @@ -364,7 +387,7 @@ describe('instruction relation', () => { await program.methods .createInstructionRelation() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, application: newApplication.publicKey, instruction: newInstruction.publicKey, @@ -373,10 +396,10 @@ describe('instruction relation', () => { }) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6027); + assert.equal(error?.error.errorCode.number, 6027); }); it('should fail when user is not a collaborator', async () => { @@ -384,12 +407,12 @@ describe('instruction relation', () => { const newUser = Keypair.generate(); const newFrom = Keypair.generate(); const newTo = Keypair.generate(); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act await program.methods .createInstructionAccount(fromDto) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -400,7 +423,7 @@ describe('instruction relation', () => { await program.methods .createInstructionAccount(toDto) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -422,17 +445,17 @@ describe('instruction relation', () => { .signers([newUser]) .preInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newUser.publicKey, lamports: LAMPORTS_PER_SOL, }), ]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 3012); + assert.equal(error?.error.errorCode.number, 3012); }); it('should fail when user is not an approved collaborator', async () => { @@ -440,21 +463,25 @@ describe('instruction relation', () => { const newFrom = Keypair.generate(); const newTo = Keypair.generate(); const newUser = Keypair.generate(); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act const [newUserPublicKey] = await PublicKey.findProgramAddress( [Buffer.from('user', 'utf8'), newUser.publicKey.toBuffer()], program.programId ); await program.methods - .createUser() + .createUser({ + name: newUserName, + thumbnailUrl: newUserThumbnailUrl, + userName: newUserUserName, + }) .accounts({ authority: newUser.publicKey, }) .signers([newUser]) .preInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newUser.publicKey, lamports: LAMPORTS_PER_SOL, }), @@ -472,7 +499,7 @@ describe('instruction relation', () => { await program.methods .createInstructionAccount(fromDto) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -483,7 +510,7 @@ describe('instruction relation', () => { await program.methods .createInstructionAccount(toDto) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -506,9 +533,9 @@ describe('instruction relation', () => { .signers([newUser]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6029); + assert.equal(error?.error.errorCode.number, 6029); }); }); diff --git a/apps/bulldozer-programs/tests/instruction.spec.ts b/apps/bulldozer-programs/tests/instruction.spec.ts index eafffb30..2505a077 100644 --- a/apps/bulldozer-programs/tests/instruction.spec.ts +++ b/apps/bulldozer-programs/tests/instruction.spec.ts @@ -1,4 +1,4 @@ -import { Program, ProgramError, Provider } from '@heavy-duty/anchor'; +import { AnchorError, AnchorProvider, Program } from '@heavy-duty/anchor'; import { Keypair, LAMPORTS_PER_SOL, @@ -10,11 +10,8 @@ import { Bulldozer, IDL } from '../target/types/bulldozer'; import { BULLDOZER_PROGRAM_ID } from './utils'; describe('instruction', () => { - const program = new Program( - IDL, - BULLDOZER_PROGRAM_ID, - Provider.env() - ); + const provider = AnchorProvider.env(); + const program = new Program(IDL, BULLDOZER_PROGRAM_ID, provider); const instruction = Keypair.generate(); const application = Keypair.generate(); const workspace = Keypair.generate(); @@ -22,6 +19,12 @@ describe('instruction', () => { const workspaceName = 'my-workspace'; let budgetPublicKey: PublicKey; let applicationStatsPublicKey: PublicKey; + const userUserName = 'user-name-1'; + const userName = 'User Name 1'; + const userThumbnailUrl = 'https://img/1.com'; + const newUserUserName = 'user-name-2'; + const newUserName = 'User Name 2'; + const newUserThumbnailUrl = 'https://img/2.com'; before(async () => { [budgetPublicKey] = await PublicKey.findProgramAddress( @@ -38,9 +41,13 @@ describe('instruction', () => { try { await program.methods - .createUser() + .createUser({ + name: userName, + thumbnailUrl: userThumbnailUrl, + userName: userUserName, + }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, }) .rpc(); } catch (error) {} @@ -48,13 +55,13 @@ describe('instruction', () => { await program.methods .createWorkspace({ name: workspaceName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, }) .signers([workspace]) .postInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: budgetPublicKey, lamports: LAMPORTS_PER_SOL, }), @@ -63,7 +70,7 @@ describe('instruction', () => { await program.methods .createApplication({ name: applicationName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, }) @@ -78,7 +85,7 @@ describe('instruction', () => { await program.methods .createInstruction({ name: instructionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -91,7 +98,7 @@ describe('instruction', () => { ); const applicationStatsAccount = await program.account.applicationStats.fetch(applicationStatsPublicKey); - assert.ok(account.authority.equals(program.provider.wallet.publicKey)); + assert.ok(account.authority.equals(provider.wallet.publicKey)); assert.equal(account.name, instructionName); assert.equal(account.body, ''); assert.ok(account.workspace.equals(workspace.publicKey)); @@ -107,7 +114,7 @@ describe('instruction', () => { await program.methods .updateInstruction({ name: instructionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -136,7 +143,7 @@ describe('instruction', () => { await program.methods .updateInstructionBody({ body: instructionBody }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -154,7 +161,7 @@ describe('instruction', () => { await program.methods .deleteInstruction() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, instruction: instruction.publicKey, application: application.publicKey, @@ -183,13 +190,13 @@ describe('instruction', () => { max: null, maxLength: null, }; - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods .createInstruction({ name: instructionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -199,7 +206,7 @@ describe('instruction', () => { await program.methods .createInstructionArgument(argumentsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -210,17 +217,17 @@ describe('instruction', () => { await program.methods .deleteInstruction() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, }) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6016); + assert.equal(error?.error.errorCode.number, 6016); }); it('should fail when deleting instruction with accounts', async () => { @@ -234,13 +241,13 @@ describe('instruction', () => { modifier: null, space: null, }; - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods .createInstruction({ name: instructionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -250,7 +257,7 @@ describe('instruction', () => { await program.methods .createInstructionAccount(argumentsData) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, @@ -261,17 +268,17 @@ describe('instruction', () => { await program.methods .deleteInstruction() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: instruction.publicKey, }) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6018); + assert.equal(error?.error.errorCode.number, 6018); }); it('should fail when providing wrong "application" to delete', async () => { @@ -280,13 +287,13 @@ describe('instruction', () => { const newApplicationName = 'sample'; const newInstruction = Keypair.generate(); const newInstructionName = 'sample'; - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods .createApplication({ name: newApplicationName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: newApplication.publicKey, }) @@ -295,7 +302,7 @@ describe('instruction', () => { await program.methods .createInstruction({ name: newInstructionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: newApplication.publicKey, instruction: newInstruction.publicKey, @@ -305,17 +312,17 @@ describe('instruction', () => { await program.methods .deleteInstruction() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, application: application.publicKey, instruction: newInstruction.publicKey, }) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6039); + assert.equal(error?.error.errorCode.number, 6039); }); it('should fail when workspace has insufficient funds', async () => { @@ -330,18 +337,18 @@ describe('instruction', () => { [Buffer.from('budget', 'utf8'), newWorkspace.publicKey.toBuffer()], program.programId ); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act await program.methods .createWorkspace({ name: newWorkspaceName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, }) .signers([newWorkspace]) .postInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newBudgetPublicKey, lamports: (await program.provider.connection.getMinimumBalanceForRentExemption( @@ -356,7 +363,7 @@ describe('instruction', () => { await program.methods .createApplication({ name: newApplicationName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, application: newApplication.publicKey, }) @@ -366,7 +373,7 @@ describe('instruction', () => { await program.methods .createInstruction({ name: newInstructionName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, application: newApplication.publicKey, instruction: newInstruction.publicKey, @@ -374,10 +381,10 @@ describe('instruction', () => { .signers([newInstruction]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6027); + assert.equal(error?.error.errorCode.number, 6027); }); it('should fail when user is not a collaborator', async () => { @@ -385,7 +392,7 @@ describe('instruction', () => { const newInstruction = Keypair.generate(); const newInstructionName = 'sample'; const newUser = Keypair.generate(); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods @@ -399,17 +406,17 @@ describe('instruction', () => { .signers([newUser, newInstruction]) .preInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newUser.publicKey, lamports: LAMPORTS_PER_SOL, }), ]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 3012); + assert.equal(error?.error.errorCode.number, 3012); }); it('should fail when user is not an approved collaborator', async () => { @@ -417,21 +424,25 @@ describe('instruction', () => { const newInstruction = Keypair.generate(); const newInstructionName = 'sample'; const newUser = Keypair.generate(); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act const [newUserPublicKey] = await PublicKey.findProgramAddress( [Buffer.from('user', 'utf8'), newUser.publicKey.toBuffer()], program.programId ); await program.methods - .createUser() + .createUser({ + name: newUserName, + thumbnailUrl: newUserThumbnailUrl, + userName: newUserUserName, + }) .accounts({ authority: newUser.publicKey, }) .signers([newUser]) .preInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newUser.publicKey, lamports: LAMPORTS_PER_SOL, }), @@ -459,9 +470,9 @@ describe('instruction', () => { .signers([newUser, newInstruction]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6029); + assert.equal(error?.error.errorCode.number, 6029); }); }); diff --git a/apps/bulldozer-programs/tests/utils/decode-account-kind.ts b/apps/bulldozer-programs/tests/utils/decode-account-kind.ts index 5e388c50..8e43f639 100644 --- a/apps/bulldozer-programs/tests/utils/decode-account-kind.ts +++ b/apps/bulldozer-programs/tests/utils/decode-account-kind.ts @@ -1,13 +1,10 @@ -import { PublicKey } from '@solana/web3.js'; - type EncodedAccountKind = { - [key: string]: { id: number; collection: PublicKey | null }; + [key: string]: { id: number }; }; interface DecodedAccountKind { id: number; name: string; - collection: string | null; } export const decodeAccountKind = ( @@ -17,6 +14,5 @@ export const decodeAccountKind = ( return { id: accountKind[accountKindName].id, name: accountKindName, - collection: accountKind[accountKindName].collection?.toBase58() || null, }; }; diff --git a/apps/bulldozer-programs/tests/utils/decode-account-modifier.ts b/apps/bulldozer-programs/tests/utils/decode-account-modifier.ts index 12cf9709..2fb8437a 100644 --- a/apps/bulldozer-programs/tests/utils/decode-account-modifier.ts +++ b/apps/bulldozer-programs/tests/utils/decode-account-modifier.ts @@ -1,20 +1,12 @@ -import { PublicKey } from '@solana/web3.js'; - type EncodedAccountModifier = { [key: string]: { id: number; - space: number | null; - close: PublicKey | null; - payer: PublicKey | null; }; }; interface DecodedAccountModifier { id: number; name: string; - space: number | null; - close: string | null; - payer: string | null; } export const decodeAccountModifier = ( @@ -24,8 +16,5 @@ export const decodeAccountModifier = ( return { id: accountModifier[accountModifierName].id, name: accountModifierName, - space: accountModifier[accountModifierName].space || null, - close: accountModifier[accountModifierName].close?.toBase58() || null, - payer: accountModifier[accountModifierName].payer?.toBase58() || null, }; }; diff --git a/apps/bulldozer-programs/tests/workspace.spec.ts b/apps/bulldozer-programs/tests/workspace.spec.ts index c232f9f1..7fad680f 100644 --- a/apps/bulldozer-programs/tests/workspace.spec.ts +++ b/apps/bulldozer-programs/tests/workspace.spec.ts @@ -1,4 +1,4 @@ -import { Program, ProgramError, Provider } from '@heavy-duty/anchor'; +import { AnchorError, AnchorProvider, Program } from '@heavy-duty/anchor'; import { Keypair, LAMPORTS_PER_SOL, @@ -10,60 +10,59 @@ import { Bulldozer, IDL } from '../target/types/bulldozer'; import { BULLDOZER_PROGRAM_ID } from './utils'; describe('workspace', () => { - const program = new Program( - IDL, - BULLDOZER_PROGRAM_ID, - Provider.env() - ); + const provider = AnchorProvider.env(); + const program = new Program(IDL, BULLDOZER_PROGRAM_ID, provider); const workspace = Keypair.generate(); const newUser = Keypair.generate(); let userPublicKey: PublicKey; let newUserPublicKey: PublicKey; - let collaboratorPublicKey: PublicKey; let budgetPublicKey: PublicKey; + const userUserName = 'user-name-1'; + const userName = 'User Name 1'; + const userThumbnailUrl = 'https://img/1.com'; + const newUserUserName = 'user-name-2'; + const newUserName = 'User Name 2'; + const newUserThumbnailUrl = 'https://img/2.com'; before(async () => { [userPublicKey] = await PublicKey.findProgramAddress( - [ - Buffer.from('user', 'utf8'), - program.provider.wallet.publicKey.toBuffer(), - ], + [Buffer.from('user', 'utf8'), provider.wallet.publicKey.toBuffer()], program.programId ); [newUserPublicKey] = await PublicKey.findProgramAddress( [Buffer.from('user', 'utf8'), newUser.publicKey.toBuffer()], program.programId ); - [collaboratorPublicKey] = await PublicKey.findProgramAddress( - [ - Buffer.from('collaborator', 'utf8'), - workspace.publicKey.toBuffer(), - userPublicKey.toBuffer(), - ], - program.programId - ); [budgetPublicKey] = await PublicKey.findProgramAddress( [Buffer.from('budget', 'utf8'), workspace.publicKey.toBuffer()], program.programId ); try { await program.methods - .createUser() + .createUser({ + name: userName, + thumbnailUrl: userThumbnailUrl, + userName: userUserName, + }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, }) .rpc(); } catch (error) {} await program.methods - .createUser() + .createUser({ + name: newUserName, + thumbnailUrl: newUserThumbnailUrl, + userName: newUserUserName, + }) .accounts({ authority: newUser.publicKey, }) .signers([newUser]) .preInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newUser.publicKey, lamports: LAMPORTS_PER_SOL, }), @@ -80,12 +79,12 @@ describe('workspace', () => { name: workspaceName, }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, }) .postInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: budgetPublicKey, lamports: LAMPORTS_PER_SOL, }), @@ -94,7 +93,7 @@ describe('workspace', () => { .rpc(); // assert const account = await program.account.workspace.fetch(workspace.publicKey); - assert.ok(account.authority.equals(program.provider.wallet.publicKey)); + assert.ok(account.authority.equals(provider.wallet.publicKey)); assert.equal(account.name, workspaceName); assert.ok(account.createdAt.eq(account.updatedAt)); }); @@ -106,7 +105,7 @@ describe('workspace', () => { await program.methods .updateWorkspace({ name: workspaceName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, }) .rpc(); @@ -121,7 +120,7 @@ describe('workspace', () => { await program.methods .deleteWorkspace() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: workspace.publicKey, }) .rpc(); @@ -142,7 +141,7 @@ describe('workspace', () => { [Buffer.from('budget', 'utf8'), newWorkspace.publicKey.toBuffer()], program.programId ); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods @@ -152,13 +151,13 @@ describe('workspace', () => { .accounts({ // This is temporal since anchor doesn't populate pda from a defined type argument workspace: newWorkspace.publicKey, - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, user: userPublicKey, }) .signers([newWorkspace]) .postInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newBudgetPublicKey, lamports: LAMPORTS_PER_SOL, }), @@ -167,7 +166,7 @@ describe('workspace', () => { await program.methods .createApplication({ name: applicationName }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, application: application.publicKey, }) @@ -176,22 +175,23 @@ describe('workspace', () => { await program.methods .deleteWorkspace() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, }) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } + // assert - assert.equal(error?.code, 6024); + assert.equal(error?.error.errorCode.number, 6024); }); it('should fail when deleting workspace with collaborators', async () => { // arrange const newWorkspaceName = 'sample'; const newWorkspace = Keypair.generate(); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act await program.methods .createWorkspace({ @@ -199,7 +199,7 @@ describe('workspace', () => { }) .accounts({ workspace: newWorkspace.publicKey, - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, }) .signers([newWorkspace]) .rpc(); @@ -210,29 +210,29 @@ describe('workspace', () => { .accounts({ workspace: newWorkspace.publicKey, user: newUserPublicKey, - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, }) .rpc(); await program.methods .deleteWorkspace() .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, }) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6025); + assert.equal(error?.error.errorCode.number, 6025); }); it('should fail when user is not a collaborator', async () => { // arrange const newWorkspaceName = 'sample'; const newUser = Keypair.generate(); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act try { await program.methods @@ -244,17 +244,17 @@ describe('workspace', () => { .signers([newUser]) .preInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newUser.publicKey, lamports: LAMPORTS_PER_SOL, }), ]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 3012); + assert.equal(error?.error.errorCode.number, 3012); }); it('should fail when user is not an admin collaborator', async () => { @@ -262,21 +262,25 @@ describe('workspace', () => { const newWorkspace = Keypair.generate(); const newWorkspaceName = 'sample'; const newUser = Keypair.generate(); - let error: ProgramError | null = null; + let error: AnchorError | null = null; // act const [newUserPublicKey] = await PublicKey.findProgramAddress( [Buffer.from('user', 'utf8'), newUser.publicKey.toBuffer()], program.programId ); await program.methods - .createUser() + .createUser({ + name: newUserName, + thumbnailUrl: newUserThumbnailUrl, + userName: newUserUserName, + }) .accounts({ authority: newUser.publicKey, }) .signers([newUser]) .preInstructions([ SystemProgram.transfer({ - fromPubkey: program.provider.wallet.publicKey, + fromPubkey: provider.wallet.publicKey, toPubkey: newUser.publicKey, lamports: LAMPORTS_PER_SOL, }), @@ -287,7 +291,7 @@ describe('workspace', () => { name: newWorkspaceName, }) .accounts({ - authority: program.provider.wallet.publicKey, + authority: provider.wallet.publicKey, workspace: newWorkspace.publicKey, }) .signers([newWorkspace]) @@ -311,9 +315,9 @@ describe('workspace', () => { .signers([newUser]) .rpc(); } catch (err) { - error = err as ProgramError; + error = err as AnchorError; } // assert - assert.equal(error?.code, 6045); + assert.equal(error?.error.errorCode.number, 6045); }); }); diff --git a/libs/broadcaster/data-access/src/index.ts b/libs/broadcaster/data-access/src/index.ts index 97c7d7c4..65f9ff65 100644 --- a/libs/broadcaster/data-access/src/index.ts +++ b/libs/broadcaster/data-access/src/index.ts @@ -1,3 +1,4 @@ export * from './lib/broadcaster-socket.store'; export * from './lib/broadcaster.module'; export * from './lib/broadcaster.store'; +export * from './lib/types'; diff --git a/libs/broadcaster/data-access/src/lib/broadcaster-socket.store.ts b/libs/broadcaster/data-access/src/lib/broadcaster-socket.store.ts index c39be95b..fa71fc97 100644 --- a/libs/broadcaster/data-access/src/lib/broadcaster-socket.store.ts +++ b/libs/broadcaster/data-access/src/lib/broadcaster-socket.store.ts @@ -1,21 +1,15 @@ -import { Injectable } from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; import { WebSocketStore } from '@heavy-duty/ngx-websocket'; +import { HdBroadcasterConfig, HD_BROADCASTER_CONFIG } from './config'; @Injectable() export class HdBroadcasterSocketStore extends WebSocketStore { - constructor() { - super({ - reconnection: true, - reconnectionDelay: 1_000, // 1 second - reconnectionDelayMax: 300_000, // 5 minutes - heartBeatDelay: 30_000, // 30 seconds - heartBeatMessage: JSON.stringify({ - jsonrpc: '2.0', - method: 'ping', - params: null, - }), - }); + constructor( + @Inject(HD_BROADCASTER_CONFIG) + hdBroadcasterConfig: HdBroadcasterConfig + ) { + super(hdBroadcasterConfig.webSocketConfig); - this.setEndpoint('ws://localhost:3333'); + this.setEndpoint(hdBroadcasterConfig.url); } } diff --git a/libs/broadcaster/data-access/src/lib/broadcaster.module.ts b/libs/broadcaster/data-access/src/lib/broadcaster.module.ts index 4427c4dd..a1f4657d 100644 --- a/libs/broadcaster/data-access/src/lib/broadcaster.module.ts +++ b/libs/broadcaster/data-access/src/lib/broadcaster.module.ts @@ -1,16 +1,38 @@ import { HttpClientModule } from '@angular/common/http'; import { ModuleWithProviders, NgModule } from '@angular/core'; +import { WebSocketConfig, WebSocketEndpoint } from '@heavy-duty/ngx-websocket'; import { HdBroadcasterSocketStore } from './broadcaster-socket.store'; import { HdBroadcasterStore } from './broadcaster.store'; +import { hdBroadcasterConfigProviderFactory } from './config'; @NgModule({ imports: [HttpClientModule], }) export class HdBroadcasterModule { - static forRoot(): ModuleWithProviders { + static forRoot( + url: WebSocketEndpoint, + webSocketConfig?: WebSocketConfig + ): ModuleWithProviders { return { ngModule: HdBroadcasterModule, - providers: [HdBroadcasterSocketStore, HdBroadcasterStore], + providers: [ + hdBroadcasterConfigProviderFactory({ + url, + webSocketConfig: webSocketConfig ?? { + reconnection: true, + reconnectionDelay: 1_000, // 1 second + reconnectionDelayMax: 300_000, // 5 minutes + heartBeatDelay: 30_000, // 30 seconds + heartBeatMessage: JSON.stringify({ + jsonrpc: '2.0', + method: 'ping', + params: null, + }), + }, + }), + HdBroadcasterSocketStore, + HdBroadcasterStore, + ], }; } } diff --git a/libs/broadcaster/data-access/src/lib/broadcaster.store.ts b/libs/broadcaster/data-access/src/lib/broadcaster.store.ts index 455dec47..6d2fe6eb 100644 --- a/libs/broadcaster/data-access/src/lib/broadcaster.store.ts +++ b/libs/broadcaster/data-access/src/lib/broadcaster.store.ts @@ -1,28 +1,30 @@ import { Injectable } from '@angular/core'; -import { - HdSolanaApiService, - TransactionResponse, -} from '@heavy-duty/ngx-solana'; -import { ComponentStore, tapResponse } from '@ngrx/component-store'; +import { TransactionResponse } from '@heavy-duty/ngx-solana'; +import { ComponentStore } from '@ngrx/component-store'; import { Finality, Transaction, TransactionSignature } from '@solana/web3.js'; -import { map, mergeMap } from 'rxjs'; import { HdBroadcasterSocketStore } from './broadcaster-socket.store'; -export interface TransactionStatus { +export interface TransactionStatus2 { signature: TransactionSignature; status: Finality; topic: string; - transactionResponse?: TransactionResponse; + transactionResponse: TransactionResponse; + confirmedAt: number; } interface ViewModel { - transactionStatusMap: Map; + pendingTransactionStatusMap: Map< + TransactionSignature, + TransactionStatus2 + > | null; + transactionStatusMap: Map; error?: unknown; - lastTransactionStatus: TransactionStatus | null; + lastTransactionStatus: TransactionStatus2 | null; } const initialState: ViewModel = { - transactionStatusMap: new Map(), + pendingTransactionStatusMap: null, + transactionStatusMap: new Map(), lastTransactionStatus: null, }; @@ -31,6 +33,9 @@ export class HdBroadcasterStore extends ComponentStore { readonly transactionStatusMap$ = this.select( ({ transactionStatusMap }) => transactionStatusMap ); + readonly pendingTransactionStatusMap$ = this.select( + ({ pendingTransactionStatusMap }) => pendingTransactionStatusMap + ); readonly transactionStatuses$ = this.select( this.transactionStatusMap$, (transactionStatusMap) => @@ -39,6 +44,16 @@ export class HdBroadcasterStore extends ComponentStore { ([, transactionStatus]) => transactionStatus ) ); + readonly pendingTransactionStatuses$ = this.select( + this.pendingTransactionStatusMap$, + (pendingTransactionStatusMap) => + pendingTransactionStatusMap + ? Array.from( + pendingTransactionStatusMap, + ([, pendingTransactionStatus]) => pendingTransactionStatus + ) + : null + ); readonly transactionsInProcess$ = this.select( this.transactionStatuses$, (transactionStatuses) => @@ -51,117 +66,11 @@ export class HdBroadcasterStore extends ComponentStore { ); constructor( - private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, - private readonly _hdSolanaApiService: HdSolanaApiService + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore ) { super(initialState); - - this.handleTransactionConfirmed( - this._hdBroadcasterSocketStore - .fromEvent('transactionConfirmed') - .pipe(map((message) => message.data)) - ); - this._handleTransactionFinalized( - this._hdBroadcasterSocketStore - .fromEvent('transactionFinalized') - .pipe(map((message) => message.data)) - ); } - private readonly _addTransactionSignature = this.updater<{ - signature: TransactionSignature; - topic: string; - }>((state, { signature, topic }) => ({ - ...state, - transactionStatusMap: new Map( - state.transactionStatusMap.set(signature, { - signature, - topic, - status: 'confirmed', - }) - ), - })); - - private readonly _setTransactionResponse = this.updater<{ - signature: TransactionSignature; - transactionResponse: TransactionResponse; - }>((state, { signature, transactionResponse }) => { - const transactionStatus = state.transactionStatusMap.get(signature); - - if (transactionStatus === undefined) { - return state; - } - - return { - ...state, - transactionStatusMap: new Map( - state.transactionStatusMap.set(signature, { - ...transactionStatus, - transactionResponse: transactionResponse, - }) - ), - }; - }); - - private readonly _handleTransactionFinalized = this.updater<{ - signature: TransactionSignature; - topic: string; - }>((state, { signature }) => { - const transactionStatus = state.transactionStatusMap.get(signature); - - if (transactionStatus === undefined) { - return state; - } - - this.patchState({ - lastTransactionStatus: { - ...transactionStatus, - status: 'finalized', - }, - }); - - return { - ...state, - transactionStatusMap: new Map( - state.transactionStatusMap.set(signature, { - ...transactionStatus, - status: 'finalized', - }) - ), - }; - }); - - readonly handleTransactionConfirmed = this.effect<{ - signature: TransactionSignature; - topic: string; - }>( - mergeMap(({ signature, topic }) => { - this._addTransactionSignature({ signature, topic }); - - return this._hdSolanaApiService - .getTransaction(signature, 'confirmed') - .pipe( - tapResponse( - (transactionResponse) => { - this.patchState({ - lastTransactionStatus: { - topic, - signature, - transactionResponse, - status: 'confirmed', - }, - }); - this._setTransactionResponse({ - signature: signature, - transactionResponse, - }); - }, - (error) => this.patchState({ error }) - ) - ); - }) - ); - sendTransaction(transactionSignature: TransactionSignature, topic: string) { this._hdBroadcasterSocketStore.send( JSON.stringify({ @@ -191,11 +100,4 @@ export class HdBroadcasterStore extends ComponentStore { }) ); } - - clearTransactions() { - this.patchState({ - transactionStatusMap: new Map(), - lastTransactionStatus: null, - }); - } } diff --git a/libs/broadcaster/data-access/src/lib/config.ts b/libs/broadcaster/data-access/src/lib/config.ts new file mode 100644 index 00000000..292894fa --- /dev/null +++ b/libs/broadcaster/data-access/src/lib/config.ts @@ -0,0 +1,18 @@ +import { InjectionToken } from '@angular/core'; +import { WebSocketConfig, WebSocketEndpoint } from '@heavy-duty/ngx-websocket'; + +export interface HdBroadcasterConfig { + url: WebSocketEndpoint; + webSocketConfig: WebSocketConfig; +} + +export const HD_BROADCASTER_CONFIG = new InjectionToken( + 'broadcasterConfig' +); + +export const hdBroadcasterConfigProviderFactory = ( + config: HdBroadcasterConfig +) => ({ + provide: HD_BROADCASTER_CONFIG, + useValue: config, +}); diff --git a/libs/broadcaster/data-access/src/lib/types.ts b/libs/broadcaster/data-access/src/lib/types.ts new file mode 100644 index 00000000..618f26c5 --- /dev/null +++ b/libs/broadcaster/data-access/src/lib/types.ts @@ -0,0 +1,9 @@ +import { Finality, Transaction, TransactionSignature } from '@solana/web3.js'; + +export interface TransactionStatus { + signature: TransactionSignature; + status?: Finality; + error?: unknown; + transaction: Transaction; + timestamp: number; +} diff --git a/libs/broadcaster/ui/cdk/src/index.ts b/libs/broadcaster/ui/cdk/src/index.ts index 32367d2f..e8bba53b 100644 --- a/libs/broadcaster/ui/cdk/src/index.ts +++ b/libs/broadcaster/ui/cdk/src/index.ts @@ -1,2 +1,3 @@ +export * from './lib/broadcaster-status.directive'; export * from './lib/broadcaster.directive'; export * from './lib/cdk.module'; diff --git a/libs/broadcaster/ui/cdk/src/lib/broadcaster-status.directive.ts b/libs/broadcaster/ui/cdk/src/lib/broadcaster-status.directive.ts new file mode 100644 index 00000000..c86b63e7 --- /dev/null +++ b/libs/broadcaster/ui/cdk/src/lib/broadcaster-status.directive.ts @@ -0,0 +1,64 @@ +import { + ChangeDetectorRef, + Directive, + TemplateRef, + ViewContainerRef, +} from '@angular/core'; +import { HdBroadcasterSocketStore } from '@heavy-duty/broadcaster'; +import { ComponentStore } from '@ngrx/component-store'; +import { tap } from 'rxjs'; + +interface BroadcasterStatusChanges { + online: boolean; + connected: boolean; +} + +export class HdBroadcasterStatusContext implements BroadcasterStatusChanges { + $implicit!: unknown; + online = false; + connected = false; +} + +@Directive({ + selector: '[hdBroadcasterStatus]', +}) +export class HdBroadcasterStatusDirective extends ComponentStore { + private _context: HdBroadcasterStatusContext = + new HdBroadcasterStatusContext(); + private readonly _changes$ = this.select( + this._hdBroadcasterSocketStore.online$, + this._hdBroadcasterSocketStore.connected$, + (online, connected) => ({ + online, + connected, + }), + { debounce: true } + ); + + constructor( + private readonly _changeDetectionRef: ChangeDetectorRef, + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + templateRef: TemplateRef, + viewContainerRef: ViewContainerRef + ) { + super({}); + + viewContainerRef.createEmbeddedView(templateRef, this._context); + this._handleChanges(this._changes$); + } + + private readonly _handleChanges = this.effect( + tap(({ online, connected }) => { + this._context.online = online; + this._context.connected = connected; + this._changeDetectionRef.markForCheck(); + }) + ); + + static ngTemplateContextGuard( + _: HdBroadcasterStatusDirective, + ctx: unknown + ): ctx is HdBroadcasterStatusContext { + return true; + } +} diff --git a/libs/broadcaster/ui/cdk/src/lib/broadcaster.directive.ts b/libs/broadcaster/ui/cdk/src/lib/broadcaster.directive.ts index a4f94f47..f1b1e244 100644 --- a/libs/broadcaster/ui/cdk/src/lib/broadcaster.directive.ts +++ b/libs/broadcaster/ui/cdk/src/lib/broadcaster.directive.ts @@ -43,7 +43,7 @@ export class HdBroadcasterDirective extends ComponentStore { super({}); viewContainerRef.createEmbeddedView(templateRef, this._context); - this._handleChanges(this._changes$); + // this._handleChanges(this._changes$); } private readonly _handleChanges = this.effect( diff --git a/libs/broadcaster/ui/cdk/src/lib/cdk.module.ts b/libs/broadcaster/ui/cdk/src/lib/cdk.module.ts index 216c754e..b8b97be8 100644 --- a/libs/broadcaster/ui/cdk/src/lib/cdk.module.ts +++ b/libs/broadcaster/ui/cdk/src/lib/cdk.module.ts @@ -1,10 +1,11 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { HdBroadcasterStatusDirective } from './broadcaster-status.directive'; import { HdBroadcasterDirective } from './broadcaster.directive'; @NgModule({ imports: [CommonModule], - declarations: [HdBroadcasterDirective], - exports: [HdBroadcasterDirective], + declarations: [HdBroadcasterDirective, HdBroadcasterStatusDirective], + exports: [HdBroadcasterDirective, HdBroadcasterStatusDirective], }) export class HdBroadcasterCdkModule {} diff --git a/libs/broadcaster/ui/material/src/lib/material.module.ts b/libs/broadcaster/ui/material/src/lib/material.module.ts index 7e72f076..4053c039 100644 --- a/libs/broadcaster/ui/material/src/lib/material.module.ts +++ b/libs/broadcaster/ui/material/src/lib/material.module.ts @@ -6,7 +6,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { HdBroadcasterCdkModule } from '@heavy-duty/broadcaster-cdk'; import { HdTransactionsListButtonComponent } from './transactions-list-button.component'; import { HdTransactionsListComponent } from './transactions-list.component'; -import { HdTransactionsListTriggerDirective } from './transactions-list.trigger.directive'; +import { HdTransactionsListDirective } from './transactions-list.trigger.directive'; @NgModule({ imports: [ @@ -19,12 +19,12 @@ import { HdTransactionsListTriggerDirective } from './transactions-list.trigger. declarations: [ HdTransactionsListButtonComponent, HdTransactionsListComponent, - HdTransactionsListTriggerDirective, + HdTransactionsListDirective, ], exports: [ HdTransactionsListButtonComponent, HdTransactionsListComponent, - HdTransactionsListTriggerDirective, + HdTransactionsListDirective, ], }) export class HdBroadcasterMaterialModule {} diff --git a/libs/broadcaster/ui/material/src/lib/transactions-list-button.component.ts b/libs/broadcaster/ui/material/src/lib/transactions-list-button.component.ts index cbeff7fb..fc4cb14d 100644 --- a/libs/broadcaster/ui/material/src/lib/transactions-list-button.component.ts +++ b/libs/broadcaster/ui/material/src/lib/transactions-list-button.component.ts @@ -9,7 +9,7 @@ import { Component, Input } from '@angular/core'; -
- {{ application.document.name }} + {{ application.name }} - + + + + + + + + + + + +
- - - - launch - View application - - - - @@ -143,40 +143,125 @@ export class ApplicationExplorerComponent { } readonly workspaceId$ = this._applicationExplorerStore.workspaceId$; - readonly applications$ = this._applicationsStore.applications$; + readonly applications$ = this._applicationExplorerStore.applications$; constructor( - private readonly _applicationsStore: ApplicationsStore, + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _notificationStore: NotificationStore, + private readonly _applicationApiService: ApplicationApiService, private readonly _applicationExplorerStore: ApplicationExplorerStore ) {} - onCreateApplication(workspaceId: string, applicationName: string) { - this._applicationExplorerStore.createApplication({ - workspaceId, - applicationName, - }); + onCreateApplication( + authority: string, + workspaceId: string, + applicationDto: ApplicationDto + ) { + const applicationKeypair = Keypair.generate(); + + this._applicationApiService + .create(applicationKeypair, { + applicationDto, + authority, + workspaceId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Create application request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `workspaces:${workspaceId}:applications`, + `applications:${applicationKeypair.publicKey.toBase58()}`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); } onUpdateApplication( + authority: string, workspaceId: string, applicationId: string, - applicationName: string + applicationDto: ApplicationDto ) { - this._applicationExplorerStore.updateApplication({ - workspaceId, - applicationId, - applicationName, - }); + this._applicationApiService + .update({ + authority, + workspaceId, + applicationId, + applicationDto, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Update application request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `workspaces:${workspaceId}:applications`, + `applications:${applicationId}`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); } - onDeleteApplication(workspaceId: string, applicationId: string) { - this._applicationExplorerStore.deleteApplication({ - workspaceId, - applicationId, - }); + onDeleteApplication( + authority: string, + workspaceId: string, + applicationId: string + ) { + this._applicationApiService + .delete({ + authority, + workspaceId, + applicationId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Delete application request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `workspaces:${workspaceId}:applications`, + `applications:${applicationId}`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); } - identify(_: number, application: ApplicationView) { - return application.document.id; + identify(_: number, application: ApplicationItemView) { + return application.id; } } diff --git a/libs/bulldozer/client/applications/components/application-explorer/src/lib/application-explorer.module.ts b/libs/bulldozer/client/applications/components/application-explorer/src/lib/application-explorer.module.ts index bad4f0a5..caec3f45 100644 --- a/libs/bulldozer/client/applications/components/application-explorer/src/lib/application-explorer.module.ts +++ b/libs/bulldozer/client/applications/components/application-explorer/src/lib/application-explorer.module.ts @@ -12,7 +12,7 @@ import { EditApplicationModule } from '@bulldozer-client/edit-application'; import { InstructionExplorerModule } from '@bulldozer-client/instruction-explorer'; import { ItemUpdatingModule } from '@bulldozer-client/item-updating'; import { StopPropagationModule } from '@bulldozer-client/stop-propagation'; -import { WorkspaceSelectorModule } from '@bulldozer-client/workspace-selector'; +import { HdWalletAdapterCdkModule } from '@heavy-duty/wallet-adapter-cdk'; import { ReactiveComponentModule } from '@ngrx/component'; import { ApplicationExplorerComponent } from './application-explorer.component'; @@ -31,8 +31,8 @@ import { ApplicationExplorerComponent } from './application-explorer.component'; InstructionExplorerModule, StopPropagationModule, EditApplicationModule, - WorkspaceSelectorModule, ItemUpdatingModule, + HdWalletAdapterCdkModule, ], declarations: [ApplicationExplorerComponent], exports: [ApplicationExplorerComponent], diff --git a/libs/bulldozer/client/applications/components/application-explorer/src/lib/application-explorer.store.ts b/libs/bulldozer/client/applications/components/application-explorer/src/lib/application-explorer.store.ts index 88ff2e23..f3250a61 100644 --- a/libs/bulldozer/client/applications/components/application-explorer/src/lib/application-explorer.store.ts +++ b/libs/bulldozer/client/applications/components/application-explorer/src/lib/application-explorer.store.ts @@ -1,190 +1,194 @@ import { Injectable } from '@angular/core'; +import { ApplicationsStore } from '@bulldozer-client/applications-data-access'; import { - ApplicationApiService, - ApplicationQueryStore, - ApplicationsStore, -} from '@bulldozer-client/applications-data-access'; -import { NotificationStore } from '@bulldozer-client/notifications-data-access'; -import { InstructionStatus } from '@bulldozer-client/users-data-access'; -import { WorkspaceInstructionsStore } from '@bulldozer-client/workspaces-data-access'; -import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; -import { WalletStore } from '@heavy-duty/wallet-adapter'; -import { ComponentStore, tapResponse } from '@ngrx/component-store'; + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; import { - combineLatest, - concatMap, - EMPTY, - filter, - of, - pipe, - switchMap, - tap, - withLatestFrom, -} from 'rxjs'; + Application, + Document, + flattenInstructions, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined, isTruthy } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-instructions'; +import { ApplicationItemView } from './types'; + +const documentToView = ( + document: Document +): ApplicationItemView => { + return { + id: document.id, + name: document.name, + isCreating: false, + isUpdating: false, + isDeleting: false, + workspaceId: document.data.workspace, + }; +}; interface ViewModel { workspaceId: string | null; + applicationId: string | null; + transactions: List; } const initialState: ViewModel = { workspaceId: null, + applicationId: null, + transactions: List(), }; @Injectable() export class ApplicationExplorerStore extends ComponentStore { readonly workspaceId$ = this.select(({ workspaceId }) => workspaceId); + private readonly _topicName$ = this.select( + this.workspaceId$.pipe(isNotNullOrUndefined), + (workspaceId) => `workspaces:${workspaceId}:applications` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly applications$ = this.select( + this._applicationsStore.applications$, + this._instructionStatuses$, + (applications, instructionStatuses) => { + if (applications === null) { + return null; + } + + return instructionStatuses.reduce( + reduceInstructions, + applications.map(documentToView) + ); + }, + { debounce: true } + ); constructor( - private readonly _applicationApiService: ApplicationApiService, - private readonly _applicationQueryStore: ApplicationQueryStore, - private readonly _applicationsStore: ApplicationsStore, - private readonly _notificationStore: NotificationStore, - private readonly _walletStore: WalletStore, - workspaceInstructionsStore: WorkspaceInstructionsStore + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _applicationsStore: ApplicationsStore ) { super(initialState); - this._applicationQueryStore.setFilters( - combineLatest({ - workspace: this.workspaceId$.pipe(isNotNullOrUndefined), - }) - ); - this._applicationsStore.setApplicationIds( - this._applicationQueryStore.applicationIds$ + this._applicationsStore.setFilters( + this.select( + this.workspaceId$.pipe(isNotNullOrUndefined), + this._hdBroadcasterSocketStore.connected$.pipe(isTruthy), + (workspaceId) => ({ workspace: workspaceId }) + ) ); - this._handleInstruction( - this.workspaceId$.pipe( - isNotNullOrUndefined, - switchMap((workspaceId) => - workspaceInstructionsStore.instruction$.pipe( - filter((instruction) => - instruction.accounts.some( - (account) => - account.name === 'Workspace' && account.pubkey === workspaceId - ) - ) - ) - ) + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) ) ); } + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) + ); + + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) + ); + readonly setWorkspaceId = this.updater( (state, workspaceId) => ({ ...state, workspaceId }) ); - private readonly _handleInstruction = this.effect( - tap((instructionStatus) => { - switch (instructionStatus.name) { - case 'createApplication': - case 'updateApplication': - case 'deleteApplication': { - this._applicationsStore.dispatch(instructionStatus); - break; - } - default: - break; + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); } }) ); - readonly createApplication = this.effect<{ - workspaceId: string; - applicationName: string; + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap(([{ applicationName, workspaceId }, authority]) => { - if (authority === null) { - return EMPTY; - } - - return this._applicationApiService - .create({ - applicationName, - authority: authority.toBase58(), - workspaceId, - }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent( - 'Create application request sent' - ), - (error) => this._notificationStore.setError(error) - ) - ); - }) - ) - ); + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } - readonly updateApplication = this.effect<{ - workspaceId: string; - applicationId: string; - applicationName: string; - }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap( - ([{ workspaceId, applicationId, applicationName }, authority]) => { - if (authority === null) { - return EMPTY; - } + this.patchState({ transactions: List() }); - return this._applicationApiService - .update({ - authority: authority.toBase58(), - workspaceId, - applicationName, - applicationId, - }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent( - 'Update application request sent' - ), - (error) => this._notificationStore.setError(error) - ) - ); - } - ) - ) - ); + const correlationId = uuid(); + let subscriptionId: string; - readonly deleteApplication = this.effect<{ - workspaceId: string; - applicationId: string; - }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap(([{ workspaceId, applicationId }, authority]) => { - if (authority === null) { - return EMPTY; - } - - return this._applicationApiService - .delete({ - authority: authority.toBase58(), - workspaceId, - applicationId, + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } + + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName + ); + } + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent( - 'Delete application request sent' - ), - (error) => this._notificationStore.setError(error) - ) - ); - }) - ) + ); + }) ); } diff --git a/libs/bulldozer/client/applications/components/application-explorer/src/lib/reduce-instructions.ts b/libs/bulldozer/client/applications/components/application-explorer/src/lib/reduce-instructions.ts new file mode 100644 index 00000000..243fc692 --- /dev/null +++ b/libs/bulldozer/client/applications/components/application-explorer/src/lib/reduce-instructions.ts @@ -0,0 +1,153 @@ +import { InstructionStatus } from '@heavy-duty/bulldozer-devkit'; +import { List } from 'immutable'; +import { ApplicationItemView } from './types'; + +export const reduceInstructions = ( + items: List, + instruction: InstructionStatus +): List => { + switch (instruction.name) { + case 'createApplication': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: { name: string }; + }; + const name = data.arguments.name; + + const workspaceId = instruction.accounts.find( + (account) => account.name === 'Workspace' + )?.pubkey; + const applicationId = instruction.accounts.find( + (account) => account.name === 'Application' + )?.pubkey; + + if (workspaceId === undefined || applicationId === undefined) { + throw new Error('Malformed Create Application'); + } + + const itemIndex = items.findIndex((item) => item.id === applicationId); + + if (itemIndex === -1) { + return items.push({ + id: applicationId, + name, + isCreating: true, + isUpdating: false, + isDeleting: false, + workspaceId, + }); + } else { + return items.update(itemIndex, (item) => ({ + ...item, + id: applicationId, + name, + isCreating: true, + isUpdating: false, + isDeleting: false, + workspaceId, + })); + } + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Application' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isCreating: false, + }; + }); + } else { + return items; + } + } + case 'updateApplication': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: { name: string }; + }; + const name = data.arguments.name; + + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Application' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + name, + isUpdating: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Application' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isUpdating: false, + }; + }); + } else { + return items; + } + } + case 'deleteApplication': + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Application' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isDeleting: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.filter( + (item) => + !instruction.accounts.some( + (account) => + account.name === 'Application' && account.pubkey === item.id + ) + ); + } else { + return items; + } + default: + return items; + } +}; diff --git a/libs/bulldozer/client/applications/components/application-explorer/src/lib/types.ts b/libs/bulldozer/client/applications/components/application-explorer/src/lib/types.ts new file mode 100644 index 00000000..3e389d96 --- /dev/null +++ b/libs/bulldozer/client/applications/components/application-explorer/src/lib/types.ts @@ -0,0 +1,8 @@ +export interface ApplicationItemView { + id: string; + name: string; + isCreating: boolean; + isUpdating: boolean; + isDeleting: boolean; + workspaceId: string; +} diff --git a/libs/bulldozer/client/applications/components/application-tab/src/lib/application-tab.component.ts b/libs/bulldozer/client/applications/components/application-tab/src/lib/application-tab.component.ts index 85b17339..76caf01b 100644 --- a/libs/bulldozer/client/applications/components/application-tab/src/lib/application-tab.component.ts +++ b/libs/bulldozer/client/applications/components/application-tab/src/lib/application-tab.component.ts @@ -8,16 +8,16 @@ import { ApplicationTabStore } from './application-tab.store'; template: `
- - + --> - + -->
`, providers: [ApplicationStore, ApplicationTabStore], diff --git a/libs/bulldozer/client/applications/components/application-tab/src/lib/application-tab.store.ts b/libs/bulldozer/client/applications/components/application-tab/src/lib/application-tab.store.ts index 4eb83d8e..f7e51484 100644 --- a/libs/bulldozer/client/applications/components/application-tab/src/lib/application-tab.store.ts +++ b/libs/bulldozer/client/applications/components/application-tab/src/lib/application-tab.store.ts @@ -5,7 +5,7 @@ import { InstructionStatus } from '@bulldozer-client/users-data-access'; import { WorkspaceInstructionsStore } from '@bulldozer-client/workspaces-data-access'; import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; import { ComponentStore } from '@ngrx/component-store'; -import { filter, switchMap, tap } from 'rxjs'; +import { filter, tap } from 'rxjs'; interface ViewModel { applicationId: string | null; @@ -27,7 +27,7 @@ export class ApplicationTabStore extends ComponentStore { super(initialState); this._applicationStore.setApplicationId(this.applicationId$); - this._handleInstruction( + /* this._handleInstruction( this.applicationId$.pipe( isNotNullOrUndefined, switchMap((applicationId) => @@ -42,7 +42,7 @@ export class ApplicationTabStore extends ComponentStore { ) ) ) - ); + ); */ this._handleApplicationDeleted( this.select( this.applicationId$.pipe(isNotNullOrUndefined), @@ -72,7 +72,7 @@ export class ApplicationTabStore extends ComponentStore { (state, applicationId) => ({ ...state, applicationId }) ); - private readonly _handleInstruction = this.effect( + /* private readonly _handleInstruction = this.effect( tap((instructionStatus) => { switch (instructionStatus.name) { case 'createApplication': @@ -85,7 +85,7 @@ export class ApplicationTabStore extends ComponentStore { break; } }) - ); + ); */ private readonly _handleApplicationDeleted = this.effect<{ applicationId: string; diff --git a/libs/bulldozer/client/applications/data-access/src/lib/application-api.service.ts b/libs/bulldozer/client/applications/data-access/src/lib/application-api.service.ts index aecf587f..4c092287 100644 --- a/libs/bulldozer/client/applications/data-access/src/lib/application-api.service.ts +++ b/libs/bulldozer/client/applications/data-access/src/lib/application-api.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@angular/core'; -import { HdBroadcasterStore } from '@heavy-duty/broadcaster'; import { Application, ApplicationFilters, @@ -30,7 +29,6 @@ import { first, map, Observable, - tap, throwError, } from 'rxjs'; @@ -38,8 +36,7 @@ import { export class ApplicationApiService { constructor( private readonly _hdSolanaApiService: HdSolanaApiService, - private readonly _hdSolanaConfigStore: HdSolanaConfigStore, - private readonly _hdBroadcasterStore: HdBroadcasterStore + private readonly _hdSolanaConfigStore: HdSolanaConfigStore ) {} private handleError(error: string) { @@ -101,9 +98,10 @@ export class ApplicationApiService { } // create application - create(params: Omit) { - const applicationKeypair = Keypair.generate(); - + create( + applicationKeypair: Keypair, + params: Omit + ) { return this._hdSolanaApiService.createTransaction(params.authority).pipe( addInstructionToTransaction( this._hdSolanaConfigStore.apiEndpoint$.pipe( @@ -123,12 +121,10 @@ export class ApplicationApiService { partiallySignTransaction(applicationKeypair), concatMap((transaction) => this._hdSolanaApiService.sendTransaction(transaction).pipe( - tap((transactionSignature) => - this._hdBroadcasterStore.sendTransaction( - transactionSignature, - params.workspaceId - ) - ), + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), catchError((error) => this.handleError(error)) ) ) @@ -152,12 +148,10 @@ export class ApplicationApiService { ), concatMap((transaction) => this._hdSolanaApiService.sendTransaction(transaction).pipe( - tap((transactionSignature) => - this._hdBroadcasterStore.sendTransaction( - transactionSignature, - params.workspaceId - ) - ), + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), catchError((error) => this.handleError(error)) ) ) @@ -181,12 +175,10 @@ export class ApplicationApiService { ), concatMap((transaction) => this._hdSolanaApiService.sendTransaction(transaction).pipe( - tap((transactionSignature) => - this._hdBroadcasterStore.sendTransaction( - transactionSignature, - params.workspaceId - ) - ), + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), catchError((error) => this.handleError(error)) ) ) diff --git a/libs/bulldozer/client/applications/data-access/src/lib/application.store.ts b/libs/bulldozer/client/applications/data-access/src/lib/application.store.ts index 33bb87b4..43a7bbd5 100644 --- a/libs/bulldozer/client/applications/data-access/src/lib/application.store.ts +++ b/libs/bulldozer/client/applications/data-access/src/lib/application.store.ts @@ -1,40 +1,27 @@ import { Injectable } from '@angular/core'; import { NotificationStore } from '@bulldozer-client/notifications-data-access'; -import { InstructionStatus } from '@bulldozer-client/users-data-access'; import { Application, Document } from '@heavy-duty/bulldozer-devkit'; import { ComponentStore, tapResponse } from '@ngrx/component-store'; -import { - BehaviorSubject, - combineLatest, - concatMap, - EMPTY, - map, - switchMap, -} from 'rxjs'; +import { EMPTY, switchMap } from 'rxjs'; import { ApplicationApiService } from './application-api.service'; -import { ItemView } from './types'; - -export type ApplicationView = ItemView>; interface ViewModel { - applicationId: string | null; - application: ApplicationView | null; loading: boolean; + applicationId: string | null; + application: Document | null; } const initialState: ViewModel = { + loading: false, applicationId: null, application: null, - loading: false, }; @Injectable() export class ApplicationStore extends ComponentStore { - private readonly _reload = new BehaviorSubject(null); - private readonly reload$ = this._reload.asObservable(); - readonly application$ = this.select(({ application }) => application); - readonly applicationId$ = this.select(({ applicationId }) => applicationId); readonly loading$ = this.select(({ loading }) => loading); + readonly applicationId$ = this.select(({ applicationId }) => applicationId); + readonly application$ = this.select(({ application }) => application); constructor( private readonly _applicationApiService: ApplicationApiService, @@ -42,11 +29,7 @@ export class ApplicationStore extends ComponentStore { ) { super(initialState); - this._loadApplication( - combineLatest([this.applicationId$, this.reload$]).pipe( - map(([applicationId]) => applicationId) - ) - ); + this._loadApplication(this.applicationId$); } readonly setApplicationId = this.updater( @@ -56,131 +39,25 @@ export class ApplicationStore extends ComponentStore { }) ); - private readonly _patchStatus = this.updater<{ - isCreating?: boolean; - isUpdating?: boolean; - isDeleting?: boolean; - }>((state, statuses) => ({ - ...state, - application: state.application - ? { - ...state.application, - ...statuses, - } - : null, - })); - - private readonly _setApplication = this.updater( - (state, application) => ({ - ...state, - application, - }) - ); - private readonly _loadApplication = this.effect( switchMap((applicationId) => { if (applicationId === null) { - this.patchState({ application: null }); return EMPTY; } - this.patchState({ loading: true }); + this.patchState({ loading: true, application: null }); return this._applicationApiService.findById(applicationId).pipe( tapResponse( (application) => { - if (application !== null) { - this._setApplication({ - document: application, - isCreating: false, - isUpdating: false, - isDeleting: false, - }); - } - this.patchState({ loading: false }); + this.patchState({ + loading: false, + application, + }); }, - (error) => this._notificationStore.setError({ error, loading: false }) + (error) => this._notificationStore.setError({ error }) ) ); }) ); - - readonly dispatch = this.effect( - concatMap((instructionStatus) => { - const applicationAccountMeta = instructionStatus.accounts.find( - (account) => account.name === 'Application' - ); - - if (applicationAccountMeta === undefined) { - return EMPTY; - } - - switch (instructionStatus.name) { - case 'createApplication': { - if (instructionStatus.status === 'finalized') { - this._patchStatus({ isCreating: false }); - return EMPTY; - } - - return this._applicationApiService - .findById(applicationAccountMeta.pubkey, 'confirmed') - .pipe( - tapResponse( - (application) => { - if (application !== null) { - this._setApplication({ - document: application, - isCreating: true, - isUpdating: false, - isDeleting: false, - }); - } - }, - (error) => this._notificationStore.setError({ error }) - ) - ); - } - case 'updateApplication': { - if (instructionStatus.status === 'finalized') { - this._patchStatus({ isUpdating: false }); - return EMPTY; - } - - return this._applicationApiService - .findById(applicationAccountMeta.pubkey, 'confirmed') - .pipe( - tapResponse( - (application) => { - if (application !== null) { - this._setApplication({ - document: application, - isCreating: false, - isUpdating: true, - isDeleting: false, - }); - } - }, - (error) => this._notificationStore.setError({ error }) - ) - ); - } - case 'deleteApplication': { - if (instructionStatus.status === 'confirmed') { - this._patchStatus({ isDeleting: true }); - } else { - this.patchState({ application: null }); - this._patchStatus({ isDeleting: false }); - } - - return EMPTY; - } - default: - return EMPTY; - } - }) - ); - - reload() { - this._reload.next(null); - } } diff --git a/libs/bulldozer/client/applications/data-access/src/lib/applications.store.ts b/libs/bulldozer/client/applications/data-access/src/lib/applications.store.ts index eb144940..2901401a 100644 --- a/libs/bulldozer/client/applications/data-access/src/lib/applications.store.ts +++ b/libs/bulldozer/client/applications/data-access/src/lib/applications.store.ts @@ -1,30 +1,33 @@ import { Injectable } from '@angular/core'; import { NotificationStore } from '@bulldozer-client/notifications-data-access'; -import { InstructionStatus } from '@bulldozer-client/users-data-access'; -import { Application, Document } from '@heavy-duty/bulldozer-devkit'; -import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { + Application, + ApplicationFilters, + Document, +} from '@heavy-duty/bulldozer-devkit'; import { ComponentStore, tapResponse } from '@ngrx/component-store'; -import { concatMap, EMPTY, switchMap } from 'rxjs'; +import { List, Map } from 'immutable'; +import { EMPTY, switchMap } from 'rxjs'; import { ApplicationApiService } from './application-api.service'; -import { ItemView } from './types'; - -export type ApplicationItemView = ItemView>; interface ViewModel { loading: boolean; - applicationIds: string[] | null; - applicationsMap: Map; + filters: ApplicationFilters | null; + applicationIds: List | null; + applicationsMap: Map> | null; } const initialState: ViewModel = { loading: false, + filters: null, applicationIds: null, - applicationsMap: new Map(), + applicationsMap: null, }; @Injectable() export class ApplicationsStore extends ComponentStore { readonly loading$ = this.select(({ loading }) => loading); + readonly filters$ = this.select(({ filters }) => filters); readonly applicationIds$ = this.select( ({ applicationIds }) => applicationIds ); @@ -34,7 +37,11 @@ export class ApplicationsStore extends ComponentStore { readonly applications$ = this.select( this.applicationsMap$, (applicationsMap) => - Array.from(applicationsMap, ([, application]) => application) + applicationsMap === null + ? null + : applicationsMap + .toList() + .sort((a, b) => (b.createdAt.lt(a.createdAt) ? 1 : -1)) ); constructor( @@ -44,180 +51,71 @@ export class ApplicationsStore extends ComponentStore { super(initialState); this._loadApplications(this.applicationIds$); + this._loadApplicationIds(this.filters$); } - private readonly _setApplication = this.updater( - (state, newApplication) => { - const applicationsMap = new Map(state.applicationsMap); - applicationsMap.set(newApplication.document.id, newApplication); - - return { - ...state, - applicationsMap, - }; - } - ); - - private readonly _patchStatus = this.updater<{ - applicationId: string; - statuses: { - isCreating?: boolean; - isUpdating?: boolean; - isDeleting?: boolean; - }; - }>((state, { applicationId, statuses }) => { - const applicationsMap = new Map(state.applicationsMap); - const application = applicationsMap.get(applicationId); - - if (application === undefined) { - return state; - } - - return { + readonly setFilters = this.updater( + (state, filters) => ({ ...state, - applicationsMap: applicationsMap.set(applicationId, { - ...application, - ...statuses, - }), - }; - }); - - private readonly _removeApplication = this.updater( - (state, applicationId) => { - const applicationsMap = new Map(state.applicationsMap); - applicationsMap.delete(applicationId); - return { - ...state, - applicationsMap, - }; - } + filters, + applicationIds: null, + applicationsMap: null, + }) ); - private readonly _loadApplications = this.effect( - switchMap((applicationIds) => { - if (applicationIds === null) { + private readonly _loadApplicationIds = this.effect( + switchMap((filters) => { + if (filters === null) { return EMPTY; } - this.patchState({ loading: true }); + this.patchState({ + loading: true, + applicationIds: List(), + applicationsMap: null, + }); - return this._applicationApiService.findByIds(applicationIds).pipe( + return this._applicationApiService.findIds(filters).pipe( tapResponse( - (applications) => { + (applicationIds) => { this.patchState({ - loading: false, - applicationsMap: applications - .filter( - (application): application is Document => - application !== null - ) - .reduce( - (applicationsMap, application) => - applicationsMap.set(application.id, { - document: application, - isCreating: false, - isUpdating: false, - isDeleting: false, - }), - new Map() - ), + applicationIds: List(applicationIds), }); }, - (error) => this._notificationStore.setError({ error }) + (error) => this._notificationStore.setError(error) ) ); }) ); - readonly setApplicationIds = this.updater( - (state, applicationIds) => ({ - ...state, - applicationIds, - }) - ); - - readonly dispatch = this.effect( - concatMap((instructionStatus) => { - const applicationAccountMeta = instructionStatus.accounts.find( - (account) => account.name === 'Application' - ); - - if (applicationAccountMeta === undefined) { + private readonly _loadApplications = this.effect | null>( + switchMap((applicationIds) => { + if (applicationIds === null) { return EMPTY; } - switch (instructionStatus.name) { - case 'createApplication': { - if (instructionStatus.status === 'finalized') { - this._patchStatus({ - applicationId: applicationAccountMeta.pubkey, - statuses: { - isCreating: false, - }, - }); - - return EMPTY; - } - - return this._applicationApiService - .findById(applicationAccountMeta.pubkey, 'confirmed') - .pipe( - isNotNullOrUndefined, - tapResponse( - (application) => - this._setApplication({ - document: application, - isCreating: true, - isUpdating: false, - isDeleting: false, - }), - (error) => this._notificationStore.setError({ error }) - ) - ); - } - case 'updateApplication': { - if (instructionStatus.status === 'finalized') { - this._patchStatus({ - applicationId: applicationAccountMeta.pubkey, - statuses: { - isUpdating: false, - }, - }); - - return EMPTY; - } - - return this._applicationApiService - .findById(applicationAccountMeta.pubkey, 'confirmed') - .pipe( - isNotNullOrUndefined, - tapResponse( - (application) => - this._setApplication({ - document: application, - isCreating: false, - isUpdating: true, - isDeleting: false, - }), - (error) => this._notificationStore.setError({ error }) - ) - ); - } - case 'deleteApplication': { - if (instructionStatus.status === 'confirmed') { - this._patchStatus({ - applicationId: applicationAccountMeta.pubkey, - statuses: { isDeleting: true }, - }); - } else { - this._removeApplication(applicationAccountMeta.pubkey); - } - - return EMPTY; - } - default: - return EMPTY; - } + return this._applicationApiService + .findByIds(applicationIds.toArray()) + .pipe( + tapResponse( + (applications) => { + this.patchState({ + loading: false, + applicationsMap: applications + .filter( + (application): application is Document => + application !== null + ) + .reduce( + (applicationsMap, application) => + applicationsMap.set(application.id, application), + Map>() + ), + }); + }, + (error) => this._notificationStore.setError({ error }) + ) + ); }) ); } diff --git a/libs/bulldozer/client/applications/modals/edit-application/src/lib/edit-application.component.ts b/libs/bulldozer/client/applications/modals/edit-application/src/lib/edit-application.component.ts index 260baa9c..4ec2b0a1 100644 --- a/libs/bulldozer/client/applications/modals/edit-application/src/lib/edit-application.component.ts +++ b/libs/bulldozer/client/applications/modals/edit-application/src/lib/edit-application.component.ts @@ -2,13 +2,14 @@ import { Component, HostBinding, Inject } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { Application, Document } from '@heavy-duty/bulldozer-devkit'; +import { SnackBarComponent } from '@bulldozer-client/notification-snack-bar'; +import { ApplicationDto } from '@heavy-duty/bulldozer-devkit'; @Component({ selector: 'bd-edit-application', template: ` -

- {{ data?.application ? 'Edit' : 'Create' }} application +

+ {{ application ? 'Edit' : 'Create' }} application

- + + +
+
+
+
+
+
+
- - `, }) export class EditApplicationComponent { @@ -70,12 +72,10 @@ export class EditApplicationComponent { private readonly _matSnackBar: MatSnackBar, private readonly _matDialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) - public data?: { - application?: Document; - } + public application?: ApplicationDto ) { this.form = new FormGroup({ - name: new FormControl(this.data?.application?.name ?? '', { + name: new FormControl(this.application?.name ?? '', { validators: [Validators.required, Validators.maxLength(32)], }), }); @@ -88,9 +88,13 @@ export class EditApplicationComponent { if (this.form.valid) { this._matDialogRef.close(this.form.value); } else { - this._matSnackBar.open('Invalid information', 'close', { - panelClass: 'warning-snackbar', + this._matSnackBar.openFromComponent(SnackBarComponent, { duration: 5000, + data: { + title: 'Heey...', + message: 'Invalid Information', + type: 'warning', + }, }); } } diff --git a/libs/bulldozer/client/applications/modals/edit-application/src/lib/edit-application-trigger.directive.ts b/libs/bulldozer/client/applications/modals/edit-application/src/lib/edit-application.directive.ts similarity index 50% rename from libs/bulldozer/client/applications/modals/edit-application/src/lib/edit-application-trigger.directive.ts rename to libs/bulldozer/client/applications/modals/edit-application/src/lib/edit-application.directive.ts index c23e342b..8ae83466 100644 --- a/libs/bulldozer/client/applications/modals/edit-application/src/lib/edit-application-trigger.directive.ts +++ b/libs/bulldozer/client/applications/modals/edit-application/src/lib/edit-application.directive.ts @@ -6,22 +6,21 @@ import { Output, } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { Application, Document } from '@heavy-duty/bulldozer-devkit'; +import { ApplicationDto } from '@heavy-duty/bulldozer-devkit'; import { EditApplicationComponent } from './edit-application.component'; -@Directive({ selector: '[bdEditApplicationTrigger]' }) -export class EditApplicationTriggerDirective { - @Input() application?: Document; - @Output() editApplication = new EventEmitter(); +@Directive({ selector: '[bdEditApplication]' }) +export class EditApplicationDirective { + @Input() application?: ApplicationDto; + @Output() editApplication = new EventEmitter(); @HostListener('click') onClick(): void { this._matDialog - .open< + .open( EditApplicationComponent, - { application?: Document }, - { name: string } - >(EditApplicationComponent, { data: { application: this.application } }) + { data: this.application, panelClass: 'bd-bg-image-7' } + ) .afterClosed() - .subscribe((data) => data && this.editApplication.emit(data.name)); + .subscribe((data) => data && this.editApplication.emit(data)); } constructor(private readonly _matDialog: MatDialog) {} diff --git a/libs/bulldozer/client/applications/modals/edit-application/src/lib/edit-application.module.ts b/libs/bulldozer/client/applications/modals/edit-application/src/lib/edit-application.module.ts index 108fec25..9d83f4d6 100644 --- a/libs/bulldozer/client/applications/modals/edit-application/src/lib/edit-application.module.ts +++ b/libs/bulldozer/client/applications/modals/edit-application/src/lib/edit-application.module.ts @@ -6,8 +6,8 @@ import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; -import { EditApplicationTriggerDirective } from './edit-application-trigger.directive'; import { EditApplicationComponent } from './edit-application.component'; +import { EditApplicationDirective } from './edit-application.directive'; @NgModule({ imports: [ @@ -19,7 +19,7 @@ import { EditApplicationComponent } from './edit-application.component'; MatIconModule, MatInputModule, ], - declarations: [EditApplicationComponent, EditApplicationTriggerDirective], - exports: [EditApplicationTriggerDirective], + declarations: [EditApplicationComponent, EditApplicationDirective], + exports: [EditApplicationDirective], }) export class EditApplicationModule {} diff --git a/libs/bulldozer/client/applications/pages/view-application/src/lib/view-application.component.ts b/libs/bulldozer/client/applications/pages/view-application/src/lib/view-application.component.ts index 23ed6b65..aada2b3f 100644 --- a/libs/bulldozer/client/applications/pages/view-application/src/lib/view-application.component.ts +++ b/libs/bulldozer/client/applications/pages/view-application/src/lib/view-application.component.ts @@ -10,7 +10,7 @@ import { ViewApplicationStore } from './view-application.store';

- - + -->

Visualize all the details about this application.

diff --git a/libs/bulldozer/client/applications/pages/view-application/src/lib/view-application.store.ts b/libs/bulldozer/client/applications/pages/view-application/src/lib/view-application.store.ts index 638aa09d..031ba044 100644 --- a/libs/bulldozer/client/applications/pages/view-application/src/lib/view-application.store.ts +++ b/libs/bulldozer/client/applications/pages/view-application/src/lib/view-application.store.ts @@ -1,11 +1,9 @@ import { Injectable } from '@angular/core'; import { ApplicationStore } from '@bulldozer-client/applications-data-access'; import { TabStore } from '@bulldozer-client/core-data-access'; -import { InstructionStatus } from '@bulldozer-client/users-data-access'; import { WorkspaceInstructionsStore } from '@bulldozer-client/workspaces-data-access'; -import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; import { ComponentStore } from '@ngrx/component-store'; -import { filter, switchMap, tap } from 'rxjs'; +import { tap } from 'rxjs'; interface ViewModel { workspaceId: string | null; @@ -41,7 +39,7 @@ export class ViewApplicationStore extends ComponentStore { { debounce: true } ) ); - this._handleInstruction( + /* this._handleInstruction( this.applicationId$.pipe( isNotNullOrUndefined, switchMap((applicationId) => @@ -56,7 +54,7 @@ export class ViewApplicationStore extends ComponentStore { ) ) ) - ); + ); */ } readonly setApplicationId = this.updater( @@ -67,7 +65,7 @@ export class ViewApplicationStore extends ComponentStore { (state, workspaceId) => ({ ...state, workspaceId }) ); - private readonly _handleInstruction = this.effect( + /* private readonly _handleInstruction = this.effect( tap((instructionStatus) => { switch (instructionStatus.name) { case 'createApplication': @@ -80,7 +78,7 @@ export class ViewApplicationStore extends ComponentStore { break; } }) - ); + ); */ private readonly _openTab = this.effect<{ applicationId: string | null; diff --git a/libs/bulldozer/client/auth/data-access/.eslintrc.json b/libs/bulldozer/client/auth/data-access/.eslintrc.json new file mode 100644 index 00000000..4b60b879 --- /dev/null +++ b/libs/bulldozer/client/auth/data-access/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "extends": ["../../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": [ + "plugin:@nrwl/nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "heavyDuty", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "heavy-duty", + "style": "kebab-case" + } + ] + } + }, + { + "files": ["*.html"], + "extends": ["plugin:@nrwl/nx/angular-template"], + "rules": {} + } + ] +} diff --git a/libs/bulldozer/client/auth/data-access/README.md b/libs/bulldozer/client/auth/data-access/README.md new file mode 100644 index 00000000..da34e70e --- /dev/null +++ b/libs/bulldozer/client/auth/data-access/README.md @@ -0,0 +1,7 @@ +# bulldozer-client-auth-data-access + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test bulldozer-client-auth-data-access` to execute the unit tests. diff --git a/libs/bulldozer/client/auth/data-access/jest.config.js b/libs/bulldozer/client/auth/data-access/jest.config.js new file mode 100644 index 00000000..7728dce8 --- /dev/null +++ b/libs/bulldozer/client/auth/data-access/jest.config.js @@ -0,0 +1,22 @@ +module.exports = { + displayName: 'bulldozer-client-auth-data-access', + preset: '../../../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + }, + coverageDirectory: + '../../../../../coverage/libs/bulldozer/client/auth/data-access', + transform: { + '^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular', + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/libs/bulldozer/client/auth/data-access/project.json b/libs/bulldozer/client/auth/data-access/project.json new file mode 100644 index 00000000..a2c15419 --- /dev/null +++ b/libs/bulldozer/client/auth/data-access/project.json @@ -0,0 +1,26 @@ +{ + "projectType": "library", + "root": "libs/bulldozer/client/auth/data-access", + "sourceRoot": "libs/bulldozer/client/auth/data-access/src", + "prefix": "heavy-duty", + "targets": { + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/libs/bulldozer/client/auth/data-access"], + "options": { + "jestConfig": "libs/bulldozer/client/auth/data-access/jest.config.js", + "passWithNoTests": true + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "libs/bulldozer/client/auth/data-access/src/**/*.ts", + "libs/bulldozer/client/auth/data-access/src/**/*.html" + ] + } + } + }, + "tags": [] +} diff --git a/libs/bulldozer/client/auth/data-access/src/index.ts b/libs/bulldozer/client/auth/data-access/src/index.ts new file mode 100644 index 00000000..5871ae96 --- /dev/null +++ b/libs/bulldozer/client/auth/data-access/src/index.ts @@ -0,0 +1 @@ +export * from './lib/auth.store'; diff --git a/libs/bulldozer/client/auth/data-access/src/lib/auth.store.ts b/libs/bulldozer/client/auth/data-access/src/lib/auth.store.ts new file mode 100644 index 00000000..0765d368 --- /dev/null +++ b/libs/bulldozer/client/auth/data-access/src/lib/auth.store.ts @@ -0,0 +1,86 @@ +import { Injectable } from '@angular/core'; +import { NotificationStore } from '@bulldozer-client/notifications-data-access'; +import { UserApiService } from '@bulldozer-client/users-data-access'; +import { Document, findUserAddress, User } from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { WalletStore } from '@heavy-duty/wallet-adapter'; +import { ComponentStore, tapResponse } from '@ngrx/component-store'; +import { concatMap, EMPTY, map, switchMap } from 'rxjs'; + +interface ViewModel { + loading: boolean; + userId: string | null; + user: Document | null; + error: unknown | null; +} + +const initialState: ViewModel = { + userId: null, + user: null, + loading: false, + error: null, +}; + +@Injectable() +export class AuthStore extends ComponentStore { + readonly loading$ = this.select(({ loading }) => loading); + readonly user$ = this.select(({ user }) => user); + readonly userId$ = this.select(({ userId }) => userId); + + constructor( + private readonly _walletStore: WalletStore, + private readonly _userApiService: UserApiService, + private readonly _notificationStore: NotificationStore + ) { + super(initialState); + + this._loadUserId( + this._walletStore.publicKey$.pipe( + isNotNullOrUndefined, + map((publicKey) => publicKey.toBase58()) + ) + ); + this._loadUser(this.userId$); + } + + private readonly _loadUserId = this.effect( + concatMap((authority) => { + if (authority === null) { + this.patchState({ userId: null }); + return EMPTY; + } + + this.patchState({ user: null, loading: true }); + + return findUserAddress(authority).pipe( + tapResponse( + ([userId]) => this.patchState({ userId }), + (error) => this._notificationStore.setError(error) + ) + ); + }) + ); + + private readonly _loadUser = this.effect( + switchMap((userId) => { + if (userId === null) { + return EMPTY; + } + + return this._userApiService.findById(userId).pipe( + tapResponse( + (user) => { + this.patchState({ + loading: false, + user, + }); + }, + (error) => { + this.patchState({ error, loading: false }); + this._notificationStore.setError(error); + } + ) + ); + }) + ); +} diff --git a/libs/bulldozer/client/budgets/components/budget-details/src/test-setup.ts b/libs/bulldozer/client/auth/data-access/src/test-setup.ts similarity index 100% rename from libs/bulldozer/client/budgets/components/budget-details/src/test-setup.ts rename to libs/bulldozer/client/auth/data-access/src/test-setup.ts diff --git a/libs/bulldozer/client/auth/data-access/tsconfig.json b/libs/bulldozer/client/auth/data-access/tsconfig.json new file mode 100644 index 00000000..28a74596 --- /dev/null +++ b/libs/bulldozer/client/auth/data-access/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "angularCompilerOptions": { + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/libs/bulldozer/client/auth/data-access/tsconfig.lib.json b/libs/bulldozer/client/auth/data-access/tsconfig.lib.json new file mode 100644 index 00000000..8d5eec5c --- /dev/null +++ b/libs/bulldozer/client/auth/data-access/tsconfig.lib.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": ["src/test-setup.ts", "**/*.spec.ts", "**/*.test.ts"], + "include": ["**/*.ts"] +} diff --git a/libs/bulldozer/client/auth/data-access/tsconfig.spec.json b/libs/bulldozer/client/auth/data-access/tsconfig.spec.json new file mode 100644 index 00000000..51f2538d --- /dev/null +++ b/libs/bulldozer/client/auth/data-access/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/bulldozer/client/auth/pages/unauthorized-access/src/lib/unauthorized-access.component.ts b/libs/bulldozer/client/auth/pages/unauthorized-access/src/lib/unauthorized-access.component.ts index b782faf9..ebda9e85 100644 --- a/libs/bulldozer/client/auth/pages/unauthorized-access/src/lib/unauthorized-access.component.ts +++ b/libs/bulldozer/client/auth/pages/unauthorized-access/src/lib/unauthorized-access.component.ts @@ -3,7 +3,7 @@ import { Component, HostBinding, OnDestroy, - OnInit + OnInit, } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { WalletStore } from '@heavy-duty/wallet-adapter'; @@ -12,15 +12,82 @@ import { filter, Subject, takeUntil, withLatestFrom } from 'rxjs'; @Component({ selector: 'bd-unauthorized-access', template: ` -
-
- -
-

BULLDOZER

-

Connect your wallet to start building

-
-
- +
+
+
+
+
+ +
+

BULLDOZER

+
+
+
+
+

WELCOME

+

+ Bulldozer is a open source low code platform to build Solana + programs. It gives developers the ability to manage their + program’s ecosystem through a UI, hiding all the gory details. +

+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
`, styles: [], diff --git a/libs/bulldozer/client/auth/pages/unauthorized-access/src/lib/unauthorized-access.module.ts b/libs/bulldozer/client/auth/pages/unauthorized-access/src/lib/unauthorized-access.module.ts index 8a123a6d..5afb9c2d 100644 --- a/libs/bulldozer/client/auth/pages/unauthorized-access/src/lib/unauthorized-access.module.ts +++ b/libs/bulldozer/client/auth/pages/unauthorized-access/src/lib/unauthorized-access.module.ts @@ -1,6 +1,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; +import { HdWalletAdapterCdkModule } from '@heavy-duty/wallet-adapter-cdk'; import { HdWalletAdapterMaterialModule } from '@heavy-duty/wallet-adapter-material'; import { UnauthorizedAccessComponent } from './unauthorized-access.component'; @@ -11,6 +12,7 @@ import { UnauthorizedAccessComponent } from './unauthorized-access.component'; { path: '', pathMatch: 'full', component: UnauthorizedAccessComponent }, ]), HdWalletAdapterMaterialModule, + HdWalletAdapterCdkModule, ], declarations: [UnauthorizedAccessComponent], exports: [UnauthorizedAccessComponent], diff --git a/libs/bulldozer/client/budgets/components/budget-details/README.md b/libs/bulldozer/client/budgets/components/budget-details/README.md deleted file mode 100644 index 89114a37..00000000 --- a/libs/bulldozer/client/budgets/components/budget-details/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# bulldozer-client-budgets-components-budget-details - -This library was generated with [Nx](https://nx.dev). - -## Running unit tests - -Run `nx test bulldozer-client-budgets-components-budget-details` to execute the unit tests. diff --git a/libs/bulldozer/client/budgets/components/budget-details/project.json b/libs/bulldozer/client/budgets/components/budget-details/project.json deleted file mode 100644 index 689a60ec..00000000 --- a/libs/bulldozer/client/budgets/components/budget-details/project.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "projectType": "library", - "root": "libs/bulldozer/client/budgets/components/budget-details", - "sourceRoot": "libs/bulldozer/client/budgets/components/budget-details/src", - "prefix": "bd", - "targets": { - "test": { - "executor": "@nrwl/jest:jest", - "outputs": [ - "coverage/libs/bulldozer/client/budgets/components/budget-details" - ], - "options": { - "jestConfig": "libs/bulldozer/client/budgets/components/budget-details/jest.config.js", - "passWithNoTests": true - } - }, - "lint": { - "executor": "@nrwl/linter:eslint", - "options": { - "lintFilePatterns": [ - "libs/bulldozer/client/budgets/components/budget-details/src/**/*.ts", - "libs/bulldozer/client/budgets/components/budget-details/src/**/*.html" - ] - } - } - }, - "tags": [] -} diff --git a/libs/bulldozer/client/budgets/components/budget-details/src/index.ts b/libs/bulldozer/client/budgets/components/budget-details/src/index.ts deleted file mode 100644 index 39008121..00000000 --- a/libs/bulldozer/client/budgets/components/budget-details/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './lib/budget-details.module'; diff --git a/libs/bulldozer/client/budgets/components/budget-details/src/lib/budget-details.component.ts b/libs/bulldozer/client/budgets/components/budget-details/src/lib/budget-details.component.ts deleted file mode 100644 index f5bd0504..00000000 --- a/libs/bulldozer/client/budgets/components/budget-details/src/lib/budget-details.component.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - Input, - Output, -} from '@angular/core'; -import { Budget, Document } from '@heavy-duty/bulldozer-devkit'; -import { LAMPORTS_PER_SOL } from '@solana/web3.js'; - -@Component({ - selector: 'bd-budget-details', - template: ` - -
-
-

Budget

-

Visualize workspace's budget.

-
- -
-

- Minimum Balance for Rent Exemption: - - {{ minimumBalanceForRentExemption | number }} - - lamports. - - ({{ - (minimumBalanceForRentExemption ?? 0) / lamportsPerSol - | number: '1.2-9' - }} - SOL) - -

-

- Total: - - - {{ budget?.metadata?.lamports | number }} - - lamports. - - ({{ - (budget?.metadata?.lamports ?? 0) / lamportsPerSol - | number: '1.2-9' - }} - SOL) - -

-

- Total after rent exemption: - - - {{ - (budget?.metadata?.lamports ?? 0) - - (minimumBalanceForRentExemption ?? 0) | number - }} - - lamports. - - ({{ - ((budget?.metadata?.lamports ?? 0) - - (minimumBalanceForRentExemption ?? 0)) / - lamportsPerSol | number: '1.2-9' - }} - SOL) - -

-
- -
- -
-
-
- `, - styles: [], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class BudgetDetailsComponent { - readonly lamportsPerSol = LAMPORTS_PER_SOL; - @Input() budget: Document | null = null; - @Input() minimumBalanceForRentExemption: number | null = null; - @Output() depositToBudget = new EventEmitter(); - - onDepositToBudget(lamports: number) { - this.depositToBudget.emit(lamports); - } -} diff --git a/libs/bulldozer/client/budgets/components/budget-details/src/lib/budget-details.module.ts b/libs/bulldozer/client/budgets/components/budget-details/src/lib/budget-details.module.ts deleted file mode 100644 index 41e48782..00000000 --- a/libs/bulldozer/client/budgets/components/budget-details/src/lib/budget-details.module.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCardModule } from '@angular/material/card'; -import { MatIconModule } from '@angular/material/icon'; -import { DepositToBudgetComponentModule } from '@bulldozer-client/deposit-to-budget'; -import { ObscureAddressModule } from '@bulldozer-client/obscure-address'; -import { SectionHeaderModule } from '@bulldozer-client/section-header'; -import { HdWalletAdapterMaterialModule } from '@heavy-duty/wallet-adapter-material'; -import { BudgetDetailsComponent } from './budget-details.component'; - -@NgModule({ - declarations: [BudgetDetailsComponent], - imports: [ - CommonModule, - MatButtonModule, - MatCardModule, - MatIconModule, - HdWalletAdapterMaterialModule, - SectionHeaderModule, - ObscureAddressModule, - DepositToBudgetComponentModule, - ], - exports: [BudgetDetailsComponent], -}) -export class BudgetDetailsModule {} diff --git a/libs/bulldozer/client/budgets/data-access/src/lib/budget-api.service.ts b/libs/bulldozer/client/budgets/data-access/src/lib/budget-api.service.ts index 128a07ba..59a8c60d 100644 --- a/libs/bulldozer/client/budgets/data-access/src/lib/budget-api.service.ts +++ b/libs/bulldozer/client/budgets/data-access/src/lib/budget-api.service.ts @@ -1,47 +1,48 @@ import { Injectable } from '@angular/core'; import { Budget, - BudgetFilters, - budgetQueryBuilder, BUDGET_ACCOUNT_SIZE, - BULLDOZER_PROGRAM_ID, createBudgetDocument, depositToBudget, DepositToBudgetParams, Document, parseBulldozerError, + withdrawFromBudget, + WithdrawFromBudgetParams, } from '@heavy-duty/bulldozer-devkit'; -import { HdSolanaApiService } from '@heavy-duty/ngx-solana'; +import { + HdSolanaApiService, + HdSolanaConfigStore, +} from '@heavy-duty/ngx-solana'; import { addInstructionToTransaction } from '@heavy-duty/rx-solana'; -import { catchError, concatMap, map, Observable, throwError } from 'rxjs'; +import { Finality } from '@solana/web3.js'; +import { + catchError, + concatMap, + first, + map, + Observable, + throwError, +} from 'rxjs'; @Injectable({ providedIn: 'root' }) export class BudgetApiService { - constructor(private readonly _hdSolanaApiService: HdSolanaApiService) {} + constructor( + private readonly _hdSolanaApiService: HdSolanaApiService, + private readonly _hdSolanaConfigStore: HdSolanaConfigStore + ) {} private handleError(error: string) { return throwError(() => parseBulldozerError(error) ?? null); } - // get budgets - find(filters: BudgetFilters) { - const query = budgetQueryBuilder().where(filters).build(); - - return this._hdSolanaApiService - .getProgramAccounts(BULLDOZER_PROGRAM_ID.toBase58(), query) - .pipe( - map((programAccounts) => - programAccounts.map(({ pubkey, account }) => - createBudgetDocument(pubkey, account) - ) - ) - ); - } - // get budget - findById(budgetId: string): Observable | null> { + findById( + budgetId: string, + commitment: Finality = 'finalized' + ): Observable | null> { return this._hdSolanaApiService - .getAccountInfo(budgetId) + .getAccountInfo(budgetId, commitment) .pipe( map( (accountInfo) => @@ -50,14 +51,54 @@ export class BudgetApiService { ); } - // deposit depositToBudget(params: DepositToBudgetParams) { return this._hdSolanaApiService.createTransaction(params.authority).pipe( - addInstructionToTransaction(depositToBudget(params)), + addInstructionToTransaction( + this._hdSolanaConfigStore.apiEndpoint$.pipe( + first(), + concatMap((apiEndpoint) => { + if (apiEndpoint === null) { + return throwError(() => 'API endpoint missing'); + } + + return depositToBudget(apiEndpoint, params); + }) + ) + ), concatMap((transaction) => - this._hdSolanaApiService - .sendTransaction(transaction) - .pipe(catchError((error) => this.handleError(error))) + this._hdSolanaApiService.sendTransaction(transaction).pipe( + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), + catchError((error) => this.handleError(error)) + ) + ) + ); + } + + withdrawFromBudget(params: WithdrawFromBudgetParams) { + return this._hdSolanaApiService.createTransaction(params.authority).pipe( + addInstructionToTransaction( + this._hdSolanaConfigStore.apiEndpoint$.pipe( + first(), + concatMap((apiEndpoint) => { + if (apiEndpoint === null) { + return throwError(() => 'API endpoint missing'); + } + + return withdrawFromBudget(apiEndpoint, params); + }) + ) + ), + concatMap((transaction) => + this._hdSolanaApiService.sendTransaction(transaction).pipe( + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), + catchError((error) => this.handleError(error)) + ) ) ); } diff --git a/libs/bulldozer/client/budgets/data-access/src/lib/budget.store.ts b/libs/bulldozer/client/budgets/data-access/src/lib/budget.store.ts index d3f3a342..392f4ba4 100644 --- a/libs/bulldozer/client/budgets/data-access/src/lib/budget.store.ts +++ b/libs/bulldozer/client/budgets/data-access/src/lib/budget.store.ts @@ -5,17 +5,8 @@ import { Document, findBudgetAddress, } from '@heavy-duty/bulldozer-devkit'; -import { WalletStore } from '@heavy-duty/wallet-adapter'; import { ComponentStore, tapResponse } from '@ngrx/component-store'; -import { - concatMap, - EMPTY, - of, - pipe, - startWith, - switchMap, - withLatestFrom, -} from 'rxjs'; +import { concatMap, EMPTY, switchMap } from 'rxjs'; import { BudgetApiService } from './budget-api.service'; import { BudgetEventService } from './budget-event.service'; @@ -40,8 +31,7 @@ export class BudgetStore extends ComponentStore { constructor( private readonly _budgetApiService: BudgetApiService, private readonly _budgetEventService: BudgetEventService, - private readonly _notificationStore: NotificationStore, - private readonly _walletStore: WalletStore + private readonly _notificationStore: NotificationStore ) { super(initialState); @@ -49,27 +39,25 @@ export class BudgetStore extends ComponentStore { this._loadBudgetId(this.workspaceId$); } + readonly setWorkspaceId = this.updater( + (state, workspaceId) => ({ + ...state, + workspaceId, + }) + ); + private readonly _loadBudget = this.effect( switchMap((budgetId) => { if (budgetId === null) { return EMPTY; } - return this._budgetApiService - .findById(budgetId) - .pipe( - concatMap((budget) => - this._budgetEventService - .budgetChanges(budgetId) - .pipe(startWith(budget)) - ) + return this._budgetApiService.findById(budgetId).pipe( + tapResponse( + (budget) => this.patchState({ budget }), + (error) => this._notificationStore.setError({ error }) ) - .pipe( - tapResponse( - (budget) => this.patchState({ budget }), - (error) => this._notificationStore.setError({ error }) - ) - ); + ); }) ); @@ -88,41 +76,4 @@ export class BudgetStore extends ComponentStore { ); }) ); - - readonly setWorkspaceId = this.updater( - (state, workspaceId) => ({ - ...state, - workspaceId, - }) - ); - - readonly depositToBudget = this.effect<{ - budgetId: string; - lamports: number; - }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap(([{ budgetId, lamports }, authority]) => { - if (authority === null) { - return EMPTY; - } - - return this._budgetApiService - .depositToBudget({ - authority: authority.toBase58(), - budgetId, - lamports, - }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent('Create budget request sent'), - (error) => this._notificationStore.setError(error) - ) - ); - }) - ) - ); } diff --git a/libs/bulldozer/client/budgets/modals/deposit-to-budget/src/lib/deposit-to-budget-trigger.directive.ts b/libs/bulldozer/client/budgets/modals/deposit-to-budget/src/lib/deposit-to-budget-trigger.directive.ts deleted file mode 100644 index b24ef3a3..00000000 --- a/libs/bulldozer/client/budgets/modals/deposit-to-budget/src/lib/deposit-to-budget-trigger.directive.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Directive, EventEmitter, HostListener, Output } from '@angular/core'; -import { MatDialog } from '@angular/material/dialog'; -import { DepositToBudgetComponent } from './deposit-to-budget.component'; - -@Directive({ selector: '[bdDepositToBudgetTrigger]' }) -export class DepositToBudgetTriggerDirective { - @Output() depositToBudget = new EventEmitter(); - @HostListener('click') onClick(): void { - this._matDialog - .open( - DepositToBudgetComponent - ) - .afterClosed() - .subscribe((data) => data && this.depositToBudget.emit(data.lamports)); - } - - constructor(private readonly _matDialog: MatDialog) {} -} diff --git a/libs/bulldozer/client/budgets/modals/deposit-to-budget/src/lib/deposit-to-budget.component.ts b/libs/bulldozer/client/budgets/modals/deposit-to-budget/src/lib/deposit-to-budget.component.ts index caaa9994..8e5eb20f 100644 --- a/libs/bulldozer/client/budgets/modals/deposit-to-budget/src/lib/deposit-to-budget.component.ts +++ b/libs/bulldozer/client/budgets/modals/deposit-to-budget/src/lib/deposit-to-budget.component.ts @@ -2,11 +2,12 @@ import { ChangeDetectionStrategy, Component, HostBinding } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialogRef } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; +import { SnackBarComponent } from '@bulldozer-client/notification-snack-bar'; @Component({ selector: 'bd-deposit-to-budget', template: ` -

Deposit to Budget

+

Deposit to Budget

- Lamports + Amount The lamports is mandatory.The amount is mandatory. - Minimum of 1 lamport. - + + +
+
+
+
+
+
+
- - `, styles: [], changeDetection: ChangeDetectionStrategy.OnPush, @@ -69,8 +70,8 @@ export class DepositToBudgetComponent { private readonly _matDialogRef: MatDialogRef ) { this.form = new FormGroup({ - lamports: new FormControl(null, { - validators: [Validators.required, Validators.min(1)], + amount: new FormControl(null, { + validators: [Validators.required, Validators.min(0)], }), }); } @@ -82,9 +83,13 @@ export class DepositToBudgetComponent { if (this.form.valid) { this._matDialogRef.close(this.form.value); } else { - this._matSnackBar.open('Invalid information', 'close', { - panelClass: 'warning-snackbar', + this._matSnackBar.openFromComponent(SnackBarComponent, { duration: 5000, + data: { + title: 'Heey...', + message: 'Invalid Information', + type: 'warning', + }, }); } } diff --git a/libs/bulldozer/client/budgets/modals/deposit-to-budget/src/lib/deposit-to-budget.directive.ts b/libs/bulldozer/client/budgets/modals/deposit-to-budget/src/lib/deposit-to-budget.directive.ts new file mode 100644 index 00000000..025f4b41 --- /dev/null +++ b/libs/bulldozer/client/budgets/modals/deposit-to-budget/src/lib/deposit-to-budget.directive.ts @@ -0,0 +1,27 @@ +import { Directive, EventEmitter, HostListener, Output } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { BN } from '@heavy-duty/anchor'; +import { LAMPORTS_PER_SOL } from '@solana/web3.js'; +import { DepositToBudgetComponent } from './deposit-to-budget.component'; + +@Directive({ selector: '[bdDepositToBudget]' }) +export class DepositToBudgetDirective { + @Output() depositToBudget = new EventEmitter<{ amount: BN }>(); + @HostListener('click') onClick(): void { + this._matDialog + .open( + DepositToBudgetComponent, + { panelClass: 'bd-bg-image-7' } + ) + .afterClosed() + .subscribe( + (data) => + data && + this.depositToBudget.emit({ + amount: new BN(data.amount * LAMPORTS_PER_SOL), + }) + ); + } + + constructor(private readonly _matDialog: MatDialog) {} +} diff --git a/libs/bulldozer/client/budgets/modals/deposit-to-budget/src/lib/deposit-to-budget.module.ts b/libs/bulldozer/client/budgets/modals/deposit-to-budget/src/lib/deposit-to-budget.module.ts index 4a25d558..c9a01f24 100644 --- a/libs/bulldozer/client/budgets/modals/deposit-to-budget/src/lib/deposit-to-budget.module.ts +++ b/libs/bulldozer/client/budgets/modals/deposit-to-budget/src/lib/deposit-to-budget.module.ts @@ -6,8 +6,8 @@ import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; -import { DepositToBudgetTriggerDirective } from './deposit-to-budget-trigger.directive'; import { DepositToBudgetComponent } from './deposit-to-budget.component'; +import { DepositToBudgetDirective } from './deposit-to-budget.directive'; @NgModule({ imports: [ @@ -19,7 +19,7 @@ import { DepositToBudgetComponent } from './deposit-to-budget.component'; MatIconModule, MatInputModule, ], - declarations: [DepositToBudgetComponent, DepositToBudgetTriggerDirective], - exports: [DepositToBudgetTriggerDirective], + declarations: [DepositToBudgetComponent, DepositToBudgetDirective], + exports: [DepositToBudgetDirective], }) -export class DepositToBudgetComponentModule {} +export class DepositToBudgetModule {} diff --git a/libs/bulldozer/client/budgets/components/budget-details/.eslintrc.json b/libs/bulldozer/client/budgets/modals/withdraw-from-budget/.eslintrc.json similarity index 100% rename from libs/bulldozer/client/budgets/components/budget-details/.eslintrc.json rename to libs/bulldozer/client/budgets/modals/withdraw-from-budget/.eslintrc.json diff --git a/libs/bulldozer/client/budgets/modals/withdraw-from-budget/README.md b/libs/bulldozer/client/budgets/modals/withdraw-from-budget/README.md new file mode 100644 index 00000000..97598e32 --- /dev/null +++ b/libs/bulldozer/client/budgets/modals/withdraw-from-budget/README.md @@ -0,0 +1,7 @@ +# bulldozer-client-budgets-modals-withdraw-from-budget + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test bulldozer-client-budgets-modals-withdraw-from-budget` to execute the unit tests. diff --git a/libs/bulldozer/client/budgets/modals/withdraw-from-budget/jest.config.js b/libs/bulldozer/client/budgets/modals/withdraw-from-budget/jest.config.js new file mode 100644 index 00000000..01618372 --- /dev/null +++ b/libs/bulldozer/client/budgets/modals/withdraw-from-budget/jest.config.js @@ -0,0 +1,22 @@ +module.exports = { + displayName: 'bulldozer-client-budgets-modals-withdraw-from-budget', + preset: '../../../../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + }, + coverageDirectory: + '../../../../../../coverage/libs/bulldozer/client/budgets/modals/withdraw-from-budget', + transform: { + '^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular', + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/libs/bulldozer/client/budgets/modals/withdraw-from-budget/project.json b/libs/bulldozer/client/budgets/modals/withdraw-from-budget/project.json new file mode 100644 index 00000000..2e56f4df --- /dev/null +++ b/libs/bulldozer/client/budgets/modals/withdraw-from-budget/project.json @@ -0,0 +1,28 @@ +{ + "projectType": "library", + "root": "libs/bulldozer/client/budgets/modals/withdraw-from-budget", + "sourceRoot": "libs/bulldozer/client/budgets/modals/withdraw-from-budget/src", + "prefix": "bd", + "targets": { + "test": { + "executor": "@nrwl/jest:jest", + "outputs": [ + "coverage/libs/bulldozer/client/budgets/modals/withdraw-from-budget" + ], + "options": { + "jestConfig": "libs/bulldozer/client/budgets/modals/withdraw-from-budget/jest.config.js", + "passWithNoTests": true + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "libs/bulldozer/client/budgets/modals/withdraw-from-budget/src/**/*.ts", + "libs/bulldozer/client/budgets/modals/withdraw-from-budget/src/**/*.html" + ] + } + } + }, + "tags": [] +} diff --git a/libs/bulldozer/client/budgets/modals/withdraw-from-budget/src/index.ts b/libs/bulldozer/client/budgets/modals/withdraw-from-budget/src/index.ts new file mode 100644 index 00000000..04dd7f44 --- /dev/null +++ b/libs/bulldozer/client/budgets/modals/withdraw-from-budget/src/index.ts @@ -0,0 +1 @@ +export * from './lib/withdraw-from-budget.module'; diff --git a/libs/bulldozer/client/budgets/modals/withdraw-from-budget/src/lib/withdraw-from-budget.component.ts b/libs/bulldozer/client/budgets/modals/withdraw-from-budget/src/lib/withdraw-from-budget.component.ts new file mode 100644 index 00000000..3d524d48 --- /dev/null +++ b/libs/bulldozer/client/budgets/modals/withdraw-from-budget/src/lib/withdraw-from-budget.component.ts @@ -0,0 +1,95 @@ +import { ChangeDetectionStrategy, Component, HostBinding } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MatDialogRef } from '@angular/material/dialog'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { SnackBarComponent } from '@bulldozer-client/notification-snack-bar'; + +@Component({ + selector: 'bd-withdraw-from-budget', + template: ` +

Withdraw from Budget

+ +
+ + Amount + + + The amount is mandatory. + Minimum of 1 lamport. + +
+ + +
+
+
+
+
+
+
+
+ `, + styles: [], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class WithdrawFromBudgetComponent { + @HostBinding('class') class = 'block w-72 relative'; + readonly form: FormGroup; + submitted = false; + + constructor( + private readonly _matSnackBar: MatSnackBar, + private readonly _matDialogRef: MatDialogRef + ) { + this.form = new FormGroup({ + amount: new FormControl(null, { + validators: [Validators.required, Validators.min(0)], + }), + }); + } + + onWithdrawFromBudget() { + this.submitted = true; + this.form.markAllAsTouched(); + + if (this.form.valid) { + this._matDialogRef.close(this.form.value); + } else { + this._matSnackBar.openFromComponent(SnackBarComponent, { + duration: 5000, + data: { + title: 'Heey...', + message: 'Invalid Information', + type: 'warning', + }, + }); + } + } +} diff --git a/libs/bulldozer/client/budgets/modals/withdraw-from-budget/src/lib/withdraw-from-budget.directive.ts b/libs/bulldozer/client/budgets/modals/withdraw-from-budget/src/lib/withdraw-from-budget.directive.ts new file mode 100644 index 00000000..d8f37ee8 --- /dev/null +++ b/libs/bulldozer/client/budgets/modals/withdraw-from-budget/src/lib/withdraw-from-budget.directive.ts @@ -0,0 +1,27 @@ +import { Directive, EventEmitter, HostListener, Output } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { BN } from '@heavy-duty/anchor'; +import { LAMPORTS_PER_SOL } from '@solana/web3.js'; +import { WithdrawFromBudgetComponent } from './withdraw-from-budget.component'; + +@Directive({ selector: '[bdWithdrawFromBudget]' }) +export class WithdrawFromBudgetDirective { + @Output() withdrawFromBudget = new EventEmitter<{ amount: BN }>(); + @HostListener('click') onClick(): void { + this._matDialog + .open( + WithdrawFromBudgetComponent, + { panelClass: 'bd-bg-image-7' } + ) + .afterClosed() + .subscribe( + (data) => + data && + this.withdrawFromBudget.emit({ + amount: new BN(data.amount * LAMPORTS_PER_SOL), + }) + ); + } + + constructor(private readonly _matDialog: MatDialog) {} +} diff --git a/libs/bulldozer/client/budgets/modals/withdraw-from-budget/src/lib/withdraw-from-budget.module.ts b/libs/bulldozer/client/budgets/modals/withdraw-from-budget/src/lib/withdraw-from-budget.module.ts new file mode 100644 index 00000000..95005abd --- /dev/null +++ b/libs/bulldozer/client/budgets/modals/withdraw-from-budget/src/lib/withdraw-from-budget.module.ts @@ -0,0 +1,25 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { WithdrawFromBudgetComponent } from './withdraw-from-budget.component'; +import { WithdrawFromBudgetDirective } from './withdraw-from-budget.directive'; + +@NgModule({ + imports: [ + CommonModule, + ReactiveFormsModule, + MatButtonModule, + MatDialogModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + ], + declarations: [WithdrawFromBudgetComponent, WithdrawFromBudgetDirective], + exports: [WithdrawFromBudgetDirective], +}) +export class WithdrawFromBudgetModule {} diff --git a/libs/bulldozer/client/collaborators/components/collaborators-list/src/test-setup.ts b/libs/bulldozer/client/budgets/modals/withdraw-from-budget/src/test-setup.ts similarity index 100% rename from libs/bulldozer/client/collaborators/components/collaborators-list/src/test-setup.ts rename to libs/bulldozer/client/budgets/modals/withdraw-from-budget/src/test-setup.ts diff --git a/libs/bulldozer/client/budgets/components/budget-details/tsconfig.json b/libs/bulldozer/client/budgets/modals/withdraw-from-budget/tsconfig.json similarity index 100% rename from libs/bulldozer/client/budgets/components/budget-details/tsconfig.json rename to libs/bulldozer/client/budgets/modals/withdraw-from-budget/tsconfig.json diff --git a/libs/bulldozer/client/budgets/components/budget-details/tsconfig.lib.json b/libs/bulldozer/client/budgets/modals/withdraw-from-budget/tsconfig.lib.json similarity index 100% rename from libs/bulldozer/client/budgets/components/budget-details/tsconfig.lib.json rename to libs/bulldozer/client/budgets/modals/withdraw-from-budget/tsconfig.lib.json diff --git a/libs/bulldozer/client/budgets/components/budget-details/tsconfig.spec.json b/libs/bulldozer/client/budgets/modals/withdraw-from-budget/tsconfig.spec.json similarity index 100% rename from libs/bulldozer/client/budgets/components/budget-details/tsconfig.spec.json rename to libs/bulldozer/client/budgets/modals/withdraw-from-budget/tsconfig.spec.json diff --git a/libs/bulldozer/client/collaborators/components/collaborators-list/README.md b/libs/bulldozer/client/collaborators/components/collaborators-list/README.md deleted file mode 100644 index 96b7c492..00000000 --- a/libs/bulldozer/client/collaborators/components/collaborators-list/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# bulldozer-client-collaborators-components-collaborators-list - -This library was generated with [Nx](https://nx.dev). - -## Running unit tests - -Run `nx test bulldozer-client-collaborators-components-collaborators-list` to execute the unit tests. diff --git a/libs/bulldozer/client/collaborators/components/collaborators-list/jest.config.js b/libs/bulldozer/client/collaborators/components/collaborators-list/jest.config.js deleted file mode 100644 index e7665c9c..00000000 --- a/libs/bulldozer/client/collaborators/components/collaborators-list/jest.config.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = { - displayName: 'bulldozer-client-collaborators-components-collaborators-list', - preset: '../../../../../../jest.preset.js', - setupFilesAfterEnv: ['/src/test-setup.ts'], - globals: { - 'ts-jest': { - tsconfig: '/tsconfig.spec.json', - stringifyContentPathRegex: '\\.(html|svg)$', - }, - }, - coverageDirectory: - '../../../../../../coverage/libs/bulldozer/client/collaborators/components/collaborators-list', - transform: { - '^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular', - }, - transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], - snapshotSerializers: [ - 'jest-preset-angular/build/serializers/no-ng-attributes', - 'jest-preset-angular/build/serializers/ng-snapshot', - 'jest-preset-angular/build/serializers/html-comment', - ], -}; diff --git a/libs/bulldozer/client/collaborators/components/collaborators-list/project.json b/libs/bulldozer/client/collaborators/components/collaborators-list/project.json deleted file mode 100644 index d6088185..00000000 --- a/libs/bulldozer/client/collaborators/components/collaborators-list/project.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "projectType": "library", - "root": "libs/bulldozer/client/collaborators/components/collaborators-list", - "sourceRoot": "libs/bulldozer/client/collaborators/components/collaborators-list/src", - "prefix": "bd", - "targets": { - "test": { - "executor": "@nrwl/jest:jest", - "outputs": [ - "coverage/libs/bulldozer/client/collaborators/components/collaborators-list" - ], - "options": { - "jestConfig": "libs/bulldozer/client/collaborators/components/collaborators-list/jest.config.js", - "passWithNoTests": true - } - }, - "lint": { - "executor": "@nrwl/linter:eslint", - "options": { - "lintFilePatterns": [ - "libs/bulldozer/client/collaborators/components/collaborators-list/src/**/*.ts", - "libs/bulldozer/client/collaborators/components/collaborators-list/src/**/*.html" - ] - } - } - }, - "tags": [] -} diff --git a/libs/bulldozer/client/collaborators/components/collaborators-list/src/index.ts b/libs/bulldozer/client/collaborators/components/collaborators-list/src/index.ts deleted file mode 100644 index b80782da..00000000 --- a/libs/bulldozer/client/collaborators/components/collaborators-list/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './lib/collaborators-list.module'; diff --git a/libs/bulldozer/client/collaborators/components/collaborators-list/src/lib/collaborators-list.component.ts b/libs/bulldozer/client/collaborators/components/collaborators-list/src/lib/collaborators-list.component.ts deleted file mode 100644 index fb2ae08f..00000000 --- a/libs/bulldozer/client/collaborators/components/collaborators-list/src/lib/collaborators-list.component.ts +++ /dev/null @@ -1,364 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - Input, - Output, -} from '@angular/core'; -import { UserView } from '@bulldozer-client/users-data-access'; -import { Collaborator, Document } from '@heavy-duty/bulldozer-devkit'; - -@Component({ - selector: 'bd-collaborators-list', - template: ` - -
-
-

Collaborators

-

Visualize workspace's collaborators.

- -
- - - - Display pending - - - Display pending - - - - - Show rejected - - - - - -
-
- {{ i + 1 }} -
-
-

- Collaborator ID: {{ collaborator.id }} -

-

- User ID: {{ collaborator.data.user }} -

-

- Wallet: {{ collaborator.data.authority }} -

-

- admin -

-

- Collaborator since - {{ - collaborator.createdAt.toNumber() * 1000 - | date: 'mediumDate' - }} - - - • Edited on - {{ - collaborator.updatedAt.toNumber() * 1000 - | date: 'mediumDate' - }} - -

-
-
- -
- - -
- - -
-
- check_circle -

- Approved -

- -
-
- cancel -

Rejected

- - -
-

Unknown status

-
-
-
-
- - -

There's no collaborators.

-
-
- - - - -
-
- {{ i + 1 }} -
-
-

- Collaborator ID: {{ collaborator.id }} -

-

- User ID: {{ collaborator.data.user }} -

-

- Wallet: {{ collaborator.data.authority }} -

-

- admin -

-

- Collaborator since - {{ - collaborator.createdAt.toNumber() * 1000 - | date: 'mediumDate' - }} - - - • Edited on - {{ - collaborator.updatedAt.toNumber() * 1000 - | date: 'mediumDate' - }} - -

-
-
-
- - -
- - -
-
-
-
- - -

- There's no pending collaborator requests. -

-
-
-
-
- `, - styles: [], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class CollaboratorsListComponent { - @Input() showRejected = false; - @Input() mode: 'ready' | 'pending' = 'ready'; - @Input() currentUser: UserView | null = null; - @Input() currentCollaborator: Document | null = null; - @Input() readyCollaborators: Document[] | null = null; - @Input() pendingCollaborators: Document[] | null = null; - @Output() requestCollaboratorStatus = new EventEmitter(); - @Output() approveCollaboratorStatusRequest = new EventEmitter(); - @Output() grantCollaboratorStatus = new EventEmitter(); - @Output() rejectCollaboratorStatusRequest = new EventEmitter(); - @Output() revokeCollaboratorStatus = new EventEmitter(); - @Output() retryCollaboratorStatusRequest = new EventEmitter(); - @Output() setCollaboratorListMode = new EventEmitter<'ready' | 'pending'>(); - @Output() toggleShowRejected = new EventEmitter(); - - onRequestCollaboratorStatus() { - this.requestCollaboratorStatus.emit(); - } - - onRetryCollaboratorStatusRequest(collaboratorId: string) { - this.retryCollaboratorStatusRequest.emit(collaboratorId); - } - - onApproveCollaboratorStatusRequest(collaboratorId: string) { - this.approveCollaboratorStatusRequest.emit(collaboratorId); - } - - onRejectCollaboratorStatusRequest(collaboratorId: string) { - this.rejectCollaboratorStatusRequest.emit(collaboratorId); - } - - onGrantCollaboratorStatus(collaboratorId: string) { - this.grantCollaboratorStatus.emit(collaboratorId); - } - - onRevokeCollaboratorStatus(collaboratorId: string) { - this.revokeCollaboratorStatus.emit(collaboratorId); - } - - onSetCollaboratorListMode(mode: 'ready' | 'pending') { - this.setCollaboratorListMode.emit(mode); - } - - onToggleShowRejected() { - this.toggleShowRejected.emit(); - } -} diff --git a/libs/bulldozer/client/collaborators/components/collaborators-list/src/lib/collaborators-list.module.ts b/libs/bulldozer/client/collaborators/components/collaborators-list/src/lib/collaborators-list.module.ts deleted file mode 100644 index d930a7e3..00000000 --- a/libs/bulldozer/client/collaborators/components/collaborators-list/src/lib/collaborators-list.module.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCardModule } from '@angular/material/card'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatIconModule } from '@angular/material/icon'; -import { MatListModule } from '@angular/material/list'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { MatSlideToggleModule } from '@angular/material/slide-toggle'; -import { RouterModule } from '@angular/router'; -import { SectionHeaderModule } from '@bulldozer-client/section-header'; -import { CollaboratorsListComponent } from './collaborators-list.component'; - -@NgModule({ - declarations: [CollaboratorsListComponent], - imports: [ - CommonModule, - RouterModule, - MatButtonModule, - MatCardModule, - MatCheckboxModule, - MatIconModule, - MatListModule, - MatProgressSpinnerModule, - MatSlideToggleModule, - SectionHeaderModule, - ], - exports: [CollaboratorsListComponent], -}) -export class CollaboratorsListModule {} diff --git a/libs/bulldozer/client/collaborators/data-access/src/index.ts b/libs/bulldozer/client/collaborators/data-access/src/index.ts index 4fbd419f..f18cfe9c 100644 --- a/libs/bulldozer/client/collaborators/data-access/src/index.ts +++ b/libs/bulldozer/client/collaborators/data-access/src/index.ts @@ -1,4 +1,5 @@ export * from './lib/collaborator-api.service'; export * from './lib/collaborator-event.service'; +export * from './lib/collaborator-query.store'; export * from './lib/collaborator.store'; export * from './lib/collaborators.store'; diff --git a/libs/bulldozer/client/collaborators/data-access/src/lib/collaborator-api.service.ts b/libs/bulldozer/client/collaborators/data-access/src/lib/collaborator-api.service.ts index 3b984a2a..1a25c565 100644 --- a/libs/bulldozer/client/collaborators/data-access/src/lib/collaborator-api.service.ts +++ b/libs/bulldozer/client/collaborators/data-access/src/lib/collaborator-api.service.ts @@ -23,6 +23,7 @@ import { HdSolanaConfigStore, } from '@heavy-duty/ngx-solana'; import { addInstructionToTransaction } from '@heavy-duty/rx-solana'; +import { Finality } from '@solana/web3.js'; import { catchError, concatMap, @@ -43,25 +44,31 @@ export class CollaboratorApiService { return throwError(() => parseBulldozerError(error) ?? null); } - // get collaborators - find(filters: CollaboratorFilters) { + // get collaborator ids + findIds(filters: CollaboratorFilters, commitment: Finality = 'finalized') { const query = collaboratorQueryBuilder().where(filters).build(); return this._hdSolanaApiService - .getProgramAccounts(BULLDOZER_PROGRAM_ID.toBase58(), query) + .getProgramAccounts(BULLDOZER_PROGRAM_ID.toBase58(), { + ...query, + commitment, + dataSlice: { + offset: 0, + length: 0, + }, + }) .pipe( - map((programAccounts) => - programAccounts.map(({ pubkey, account }) => - createCollaboratorDocument(pubkey, account) - ) - ) + map((programAccounts) => programAccounts.map(({ pubkey }) => pubkey)) ); } // get collaborator - findById(collaboratorId: string): Observable | null> { + findById( + collaboratorId: string, + commitment: Finality = 'finalized' + ): Observable | null> { return this._hdSolanaApiService - .getAccountInfo(collaboratorId) + .getAccountInfo(collaboratorId, commitment) .pipe( map( (accountInfo) => @@ -71,6 +78,27 @@ export class CollaboratorApiService { ); } + // get collaborators + findByIds( + applicationIds: string[], + commitment: Finality = 'finalized' + ): Observable<(Document | null)[]> { + return this._hdSolanaApiService + .getMultipleAccounts(applicationIds, { commitment }) + .pipe( + map((keyedAccounts) => + keyedAccounts.map( + (keyedAccount) => + keyedAccount && + createCollaboratorDocument( + keyedAccount.accountId, + keyedAccount.accountInfo + ) + ) + ) + ); + } + // create collaborator create(params: CreateCollaboratorParams) { return this._hdSolanaApiService.createTransaction(params.authority).pipe( @@ -87,9 +115,13 @@ export class CollaboratorApiService { ) ), concatMap((transaction) => - this._hdSolanaApiService - .sendTransaction(transaction) - .pipe(catchError((error) => this.handleError(error))) + this._hdSolanaApiService.sendTransaction(transaction).pipe( + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), + catchError((error) => this.handleError(error)) + ) ) ); } @@ -110,9 +142,13 @@ export class CollaboratorApiService { ) ), concatMap((transaction) => - this._hdSolanaApiService - .sendTransaction(transaction) - .pipe(catchError((error) => this.handleError(error))) + this._hdSolanaApiService.sendTransaction(transaction).pipe( + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), + catchError((error) => this.handleError(error)) + ) ) ); } @@ -133,9 +169,13 @@ export class CollaboratorApiService { ) ), concatMap((transaction) => - this._hdSolanaApiService - .sendTransaction(transaction) - .pipe(catchError((error) => this.handleError(error))) + this._hdSolanaApiService.sendTransaction(transaction).pipe( + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), + catchError((error) => this.handleError(error)) + ) ) ); } @@ -156,9 +196,13 @@ export class CollaboratorApiService { ) ), concatMap((transaction) => - this._hdSolanaApiService - .sendTransaction(transaction) - .pipe(catchError((error) => this.handleError(error))) + this._hdSolanaApiService.sendTransaction(transaction).pipe( + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), + catchError((error) => this.handleError(error)) + ) ) ); } @@ -179,9 +223,13 @@ export class CollaboratorApiService { ) ), concatMap((transaction) => - this._hdSolanaApiService - .sendTransaction(transaction) - .pipe(catchError((error) => this.handleError(error))) + this._hdSolanaApiService.sendTransaction(transaction).pipe( + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), + catchError((error) => this.handleError(error)) + ) ) ); } diff --git a/libs/bulldozer/client/collaborators/data-access/src/lib/collaborator-query.store.ts b/libs/bulldozer/client/collaborators/data-access/src/lib/collaborator-query.store.ts new file mode 100644 index 00000000..8620890a --- /dev/null +++ b/libs/bulldozer/client/collaborators/data-access/src/lib/collaborator-query.store.ts @@ -0,0 +1,91 @@ +import { Injectable } from '@angular/core'; +import { NotificationStore } from '@bulldozer-client/notifications-data-access'; +import { CollaboratorFilters } from '@heavy-duty/bulldozer-devkit'; +import { ComponentStore, tapResponse } from '@ngrx/component-store'; +import { EMPTY, switchMap } from 'rxjs'; +import { CollaboratorApiService } from './collaborator-api.service'; + +interface ViewModel { + loading: boolean; + collaboratorIds: string[] | null; + filters: CollaboratorFilters | null; + error: unknown | null; +} + +const initialState: ViewModel = { + loading: false, + filters: null, + collaboratorIds: null, + error: null, +}; + +@Injectable() +export class CollaboratorQueryStore extends ComponentStore { + readonly loading$ = this.select(({ loading }) => loading); + readonly filters$ = this.select(({ filters }) => filters); + readonly collaboratorIds$ = this.select( + ({ collaboratorIds }) => collaboratorIds + ); + + constructor( + private readonly _collaboratorApiService: CollaboratorApiService, + private readonly _notificationStore: NotificationStore + ) { + super(initialState); + + this._loadCollaboratorIds(this.filters$); + } + + readonly addCollaborator = this.updater( + (state, newCollaboratorId) => ({ + ...state, + collaboratorIds: [...(state.collaboratorIds ?? []), newCollaboratorId], + }) + ); + + readonly removeCollaboratorId = this.updater( + (state, collaboratorIdToRemove) => ({ + ...state, + collaboratorIds: + state.collaboratorIds?.filter( + (collaboratorId) => collaboratorId !== collaboratorIdToRemove + ) ?? null, + }) + ); + + readonly setFilters = this.updater( + (state, filters) => ({ + ...state, + filters, + }) + ); + + private readonly _loadCollaboratorIds = + this.effect( + switchMap((filters) => { + if (filters === null) { + return EMPTY; + } + + this.patchState({ loading: true }); + + return this._collaboratorApiService.findIds(filters).pipe( + tapResponse( + (collaboratorIds) => { + this.patchState({ + collaboratorIds, + loading: false, + }); + }, + (error) => { + this.patchState({ + error, + loading: false, + }); + this._notificationStore.setError(error); + } + ) + ); + }) + ); +} diff --git a/libs/bulldozer/client/collaborators/data-access/src/lib/collaborator.store.ts b/libs/bulldozer/client/collaborators/data-access/src/lib/collaborator.store.ts index 59d54dc5..6c449f04 100644 --- a/libs/bulldozer/client/collaborators/data-access/src/lib/collaborator.store.ts +++ b/libs/bulldozer/client/collaborators/data-access/src/lib/collaborator.store.ts @@ -1,121 +1,43 @@ import { Injectable } from '@angular/core'; import { NotificationStore } from '@bulldozer-client/notifications-data-access'; -import { - Collaborator, - Document, - findCollaboratorAddress, -} from '@heavy-duty/bulldozer-devkit'; -import { WalletStore } from '@heavy-duty/wallet-adapter'; +import { Collaborator, Document } from '@heavy-duty/bulldozer-devkit'; import { ComponentStore, tapResponse } from '@ngrx/component-store'; -import { PublicKey } from '@solana/web3.js'; -import { concatMap, EMPTY, switchMap } from 'rxjs'; +import { EMPTY, switchMap } from 'rxjs'; import { CollaboratorApiService } from './collaborator-api.service'; -import { CollaboratorEventService } from './collaborator-event.service'; interface ViewModel { loading: boolean; - workspaceId: string | null; - userId: string | null; collaboratorId: string | null; collaborator: Document | null; } const initialState: ViewModel = { - workspaceId: null, - userId: null, + loading: false, collaboratorId: null, collaborator: null, - loading: false, }; @Injectable() export class CollaboratorStore extends ComponentStore { readonly loading$ = this.select(({ loading }) => loading); - readonly collaborator$ = this.select(({ collaborator }) => collaborator); readonly collaboratorId$ = this.select( ({ collaboratorId }) => collaboratorId ); - readonly userId$ = this.select(({ userId }) => userId); - readonly workspaceId$ = this.select(({ workspaceId }) => workspaceId); + readonly collaborator$ = this.select(({ collaborator }) => collaborator); constructor( private readonly _collaboratorApiService: CollaboratorApiService, - private readonly _collaboratorEventService: CollaboratorEventService, - private readonly _notificationStore: NotificationStore, - private readonly _walletStore: WalletStore + private readonly _notificationStore: NotificationStore ) { super(initialState); - this._loadCollaboratorId( - this.select(this.workspaceId$, this.userId$, (workspaceId, userId) => ({ - workspaceId, - userId, - })) - ); this._loadCollaborator(this.collaboratorId$); - this._handleCollaboratorCreated(this._walletStore.publicKey$); - this._handleCollaboratorDeleted(this.collaboratorId$); } - readonly setUserId = this.updater((state, userId) => ({ - ...state, - userId, - })); - - readonly setWorkspaceId = this.updater( - (state, workspaceId) => ({ ...state, workspaceId }) - ); - - private readonly _handleCollaboratorDeleted = this.effect( - switchMap((collaboratorId) => { - if (collaboratorId === null) { - return EMPTY; - } - - return this._collaboratorEventService - .collaboratorDeleted(collaboratorId) - .pipe( - tapResponse( - () => this.patchState({ collaborator: null }), - (error) => this._notificationStore.setError(error) - ) - ); - }) - ); - - private readonly _handleCollaboratorCreated = this.effect( - switchMap((walletPublicKey) => { - if (walletPublicKey === null) { - return EMPTY; - } - - return this._collaboratorEventService - .collaboratorCreated({ authority: walletPublicKey.toBase58() }) - .pipe( - tapResponse( - (collaborator) => this.patchState({ collaborator }), - (error) => this._notificationStore.setError(error) - ) - ); - }) - ); - - private readonly _loadCollaboratorId = this.effect<{ - userId: string | null; - workspaceId: string | null; - }>( - concatMap(({ userId, workspaceId }) => { - if (userId === null || workspaceId === null) { - this.patchState({ collaboratorId: null }); - return EMPTY; - } - - return findCollaboratorAddress(workspaceId, userId).pipe( - tapResponse( - ([collaboratorId]) => this.patchState({ collaboratorId }), - (error) => this._notificationStore.setError(error) - ) - ); + readonly setCollaboratorId = this.updater( + (state, collaboratorId) => ({ + ...state, + collaboratorId, }) ); @@ -125,17 +47,15 @@ export class CollaboratorStore extends ComponentStore { return EMPTY; } - this.patchState({ loading: true }); - return this._collaboratorApiService.findById(collaboratorId).pipe( tapResponse( (collaborator) => { this.patchState({ - collaborator, loading: false, + collaborator, }); }, - (error) => this._notificationStore.setError(error) + (error) => this._notificationStore.setError({ error }) ) ); }) diff --git a/libs/bulldozer/client/collaborators/data-access/src/lib/collaborators.store.ts b/libs/bulldozer/client/collaborators/data-access/src/lib/collaborators.store.ts index a599baa5..617987ff 100644 --- a/libs/bulldozer/client/collaborators/data-access/src/lib/collaborators.store.ts +++ b/libs/bulldozer/client/collaborators/data-access/src/lib/collaborators.store.ts @@ -5,133 +5,83 @@ import { CollaboratorFilters, Document, } from '@heavy-duty/bulldozer-devkit'; -import { WalletStore } from '@heavy-duty/wallet-adapter'; import { ComponentStore, tapResponse } from '@ngrx/component-store'; -import { - concatMap, - EMPTY, - filter, - mergeMap, - of, - pipe, - switchMap, - take, - takeUntil, - takeWhile, - withLatestFrom, -} from 'rxjs'; +import { List, Map } from 'immutable'; +import { EMPTY, switchMap } from 'rxjs'; import { CollaboratorApiService } from './collaborator-api.service'; -import { CollaboratorEventService } from './collaborator-event.service'; interface ViewModel { loading: boolean; - collaboratorsMap: Map>; filters: CollaboratorFilters | null; + collaboratorIds: List | null; + collaboratorsMap: Map> | null; } const initialState: ViewModel = { loading: false, filters: null, - collaboratorsMap: new Map>(), + collaboratorIds: null, + collaboratorsMap: null, }; @Injectable() export class CollaboratorsStore extends ComponentStore { readonly loading$ = this.select(({ loading }) => loading); readonly filters$ = this.select(({ filters }) => filters); + readonly collaboratorIds$ = this.select( + ({ collaboratorIds }) => collaboratorIds + ); readonly collaboratorsMap$ = this.select( ({ collaboratorsMap }) => collaboratorsMap ); readonly collaborators$ = this.select( this.collaboratorsMap$, (collaboratorsMap) => - Array.from(collaboratorsMap, ([, collaborator]) => collaborator) + collaboratorsMap === null + ? null + : collaboratorsMap + .toList() + .sort((a, b) => (b.createdAt.lt(a.createdAt) ? 1 : -1)) ); constructor( - private readonly _walletStore: WalletStore, private readonly _collaboratorApiService: CollaboratorApiService, - private readonly _collaboratorEventService: CollaboratorEventService, private readonly _notificationStore: NotificationStore ) { super(initialState); - this._handleCollaboratorCreated(this.filters$); - this._loadCollaborators(this.filters$); + this._loadCollaborators(this.collaboratorIds$); + this._loadCollaboratorIds(this.filters$); } - private readonly _setCollaborator = this.updater>( - (state, newCollaborator) => { - const collaboratorsMap = new Map(state.collaboratorsMap); - collaboratorsMap.set(newCollaborator.id, newCollaborator); - return { - ...state, - collaboratorsMap, - }; - } - ); - - private readonly _addCollaborator = this.updater>( - (state, newCollaborator) => { - if (state.collaboratorsMap.has(newCollaborator.id)) { - return state; - } - const collaboratorsMap = new Map(state.collaboratorsMap); - collaboratorsMap.set(newCollaborator.id, newCollaborator); - return { - ...state, - collaboratorsMap, - }; - } - ); - - private readonly _removeCollaborator = this.updater( - (state, collaboratorId) => { - const collaboratorsMap = new Map(state.collaboratorsMap); - collaboratorsMap.delete(collaboratorId); - return { - ...state, - collaboratorsMap, - }; - } - ); - - private readonly _handleCollaboratorChanges = this.effect( - mergeMap((collaboratorId) => - this._collaboratorEventService.collaboratorChanges(collaboratorId).pipe( - tapResponse( - (changes) => { - if (changes === null) { - this._removeCollaborator(collaboratorId); - } else { - this._setCollaborator(changes); - } - }, - (error) => this._notificationStore.setError(error) - ), - takeUntil( - this.loading$.pipe( - filter((loading) => loading), - take(1) - ) - ), - takeWhile((collaborator) => collaborator !== null) - ) - ) + readonly setFilters = this.updater( + (state, filters) => ({ + ...state, + filters, + collaboratorIds: null, + collaboratorsMap: null, + }) ); - private readonly _handleCollaboratorCreated = + private readonly _loadCollaboratorIds = this.effect( switchMap((filters) => { if (filters === null) { return EMPTY; } - return this._collaboratorEventService.collaboratorCreated(filters).pipe( + this.patchState({ + loading: true, + collaboratorIds: List(), + collaboratorsMap: null, + }); + + return this._collaboratorApiService.findIds(filters).pipe( tapResponse( - (collaborator) => { - this._addCollaborator(collaborator); - this._handleCollaboratorChanges(collaborator.id); + (collaboratorIds) => { + this.patchState({ + collaboratorIds: List(collaboratorIds), + }); }, (error) => this._notificationStore.setError(error) ) @@ -139,103 +89,34 @@ export class CollaboratorsStore extends ComponentStore { }) ); - private readonly _loadCollaborators = this.effect( - switchMap((filters) => { - if (filters === null) { + private readonly _loadCollaborators = this.effect | null>( + switchMap((collaboratorIds) => { + if (collaboratorIds === null) { return EMPTY; } - this.patchState({ loading: true }); - - return this._collaboratorApiService.find(filters).pipe( - tapResponse( - (collaborators) => { - this.patchState({ - collaboratorsMap: collaborators.reduce( - (collaboratorsMap, collaborator) => - collaboratorsMap.set(collaborator.id, collaborator), - new Map>() - ), - loading: false, - }); - collaborators.forEach(({ id }) => - this._handleCollaboratorChanges(id) - ); - }, - (error) => this._notificationStore.setError(error) - ) - ); - }) - ); - - readonly setFilters = this.updater( - (state, filters) => ({ - ...state, - filters, + return this._collaboratorApiService + .findByIds(collaboratorIds.toArray()) + .pipe( + tapResponse( + (collaborators) => { + this.patchState({ + loading: false, + collaboratorsMap: collaborators + .filter( + (collaborator): collaborator is Document => + collaborator !== null + ) + .reduce( + (collaboratorsMap, collaborator) => + collaboratorsMap.set(collaborator.id, collaborator), + Map>() + ), + }); + }, + (error) => this._notificationStore.setError({ error }) + ) + ); }) ); - - readonly createCollaborator = this.effect<{ - workspaceId: string; - userId: string; - }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap(([{ workspaceId, userId }, authority]) => { - if (authority === null) { - return EMPTY; - } - - return this._collaboratorApiService - .create({ - authority: authority.toBase58(), - workspaceId, - userId, - }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent( - 'Create collaborator request sent' - ), - (error) => this._notificationStore.setError(error) - ) - ); - }) - ) - ); - - readonly deleteCollaborator = this.effect<{ - workspaceId: string; - collaboratorId: string; - }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap(([{ collaboratorId, workspaceId }, authority]) => { - if (authority === null) { - return EMPTY; - } - - return this._collaboratorApiService - .delete({ - authority: authority.toBase58(), - collaboratorId, - workspaceId, - }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent( - 'Delete collaborator request sent' - ), - (error) => this._notificationStore.setError(error) - ) - ); - }) - ) - ); } diff --git a/libs/bulldozer/client/collaborators/data-access/src/lib/types.ts b/libs/bulldozer/client/collaborators/data-access/src/lib/types.ts new file mode 100644 index 00000000..8d5c22e4 --- /dev/null +++ b/libs/bulldozer/client/collaborators/data-access/src/lib/types.ts @@ -0,0 +1,6 @@ +export interface ItemView { + document: T; + isCreating: boolean; + isUpdating: boolean; + isDeleting: boolean; +} diff --git a/libs/bulldozer/client/collections/components/collection-attributes-list/.browserslistrc b/libs/bulldozer/client/collections/components/collection-attributes-list/.browserslistrc deleted file mode 100644 index 4f9ac269..00000000 --- a/libs/bulldozer/client/collections/components/collection-attributes-list/.browserslistrc +++ /dev/null @@ -1,16 +0,0 @@ -# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. -# For additional information regarding the format and rule options, please see: -# https://github.com/browserslist/browserslist#queries - -# For the full list of supported browsers by the Angular framework, please see: -# https://angular.io/guide/browser-support - -# You can see what browsers were selected by your queries by running: -# npx browserslist - -last 1 Chrome version -last 1 Firefox version -last 2 Edge major versions -last 2 Safari major versions -last 2 iOS major versions -Firefox ESR diff --git a/libs/bulldozer/client/collections/components/collection-attributes-list/README.md b/libs/bulldozer/client/collections/components/collection-attributes-list/README.md deleted file mode 100644 index d3a15312..00000000 --- a/libs/bulldozer/client/collections/components/collection-attributes-list/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# bulldozer-client-collections-components-collection-attributes-list - -This library was generated with [Nx](https://nx.dev). - -## Running unit tests - -Run `nx test bulldozer-client-collections-components-collection-attributes-list` to execute the unit tests. diff --git a/libs/bulldozer/client/collections/components/collection-attributes-list/project.json b/libs/bulldozer/client/collections/components/collection-attributes-list/project.json deleted file mode 100644 index ec0c3c8a..00000000 --- a/libs/bulldozer/client/collections/components/collection-attributes-list/project.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "projectType": "library", - "root": "libs/bulldozer/client/collections/components/collection-attributes-list", - "sourceRoot": "libs/bulldozer/client/collections/components/collection-attributes-list/src", - "prefix": "bd", - "targets": { - "test": { - "executor": "@nrwl/jest:jest", - "outputs": [ - "coverage/libs/bulldozer/client/collections/components/collection-attributes-list" - ], - "options": { - "jestConfig": "libs/bulldozer/client/collections/components/collection-attributes-list/jest.config.js", - "passWithNoTests": true - } - }, - "lint": { - "executor": "@nrwl/linter:eslint", - "options": { - "lintFilePatterns": [ - "libs/bulldozer/client/collections/components/collection-attributes-list/src/**/*.ts", - "libs/bulldozer/client/collections/components/collection-attributes-list/src/**/*.html" - ] - } - } - }, - "tags": [] -} diff --git a/libs/bulldozer/client/collections/components/collection-attributes-list/src/index.ts b/libs/bulldozer/client/collections/components/collection-attributes-list/src/index.ts deleted file mode 100644 index 609b66a6..00000000 --- a/libs/bulldozer/client/collections/components/collection-attributes-list/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './lib/collection-attributes-list.module'; diff --git a/libs/bulldozer/client/collections/components/collection-attributes-list/src/lib/collection-attributes-list.component.ts b/libs/bulldozer/client/collections/components/collection-attributes-list/src/lib/collection-attributes-list.component.ts deleted file mode 100644 index 4f7be6ee..00000000 --- a/libs/bulldozer/client/collections/components/collection-attributes-list/src/lib/collection-attributes-list.component.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - Input, - Output, -} from '@angular/core'; -import { CollectionAttributeItemView } from '@bulldozer-client/collections-data-access'; -import { CollectionAttributeDto } from '@heavy-duty/bulldozer-devkit'; - -@Component({ - selector: 'bd-collection-attributes-list', - template: ` - -
-
-

- Attributes - -

-

Visualize the list of attributes and manage them.

-
- - - -
-
- {{ i + 1 }} -
-
-

- - {{ collectionAttribute.document.name }} - - -

-

- Type: - - - {{ collectionAttribute.document.data.modifier.name }} - - ({{ collectionAttribute.document.data.modifier?.size }}) - - of - - - {{ collectionAttribute.document.data.kind.name }}. -

-
- - - - - -
-
-
- - -

There's no attributes yet.

-
-
-
- `, - styles: [], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class CollectionAttributesListComponent { - @Input() connected = false; - @Input() collectionAttributes: CollectionAttributeItemView[] | null = null; - @Output() createCollectionAttribute = - new EventEmitter(); - @Output() updateCollectionAttribute = new EventEmitter<{ - collectionAttributeId: string; - collectionAttributeDto: CollectionAttributeDto; - }>(); - @Output() deleteCollectionAttribute = new EventEmitter<{ - collectionId: string; - collectionAttributeId: string; - }>(); - - onCreateCollectionAttribute(collectionAttributeDto: CollectionAttributeDto) { - this.createCollectionAttribute.emit(collectionAttributeDto); - } - - onUpdateCollectionAttribute( - collectionAttributeId: string, - collectionAttributeDto: CollectionAttributeDto - ) { - this.updateCollectionAttribute.emit({ - collectionAttributeId, - collectionAttributeDto, - }); - } - - onDeleteCollectionAttribute( - collectionId: string, - collectionAttributeId: string - ) { - this.deleteCollectionAttribute.emit({ - collectionId, - collectionAttributeId, - }); - } -} diff --git a/libs/bulldozer/client/collections/components/collection-explorer/src/lib/collection-explorer.component.ts b/libs/bulldozer/client/collections/components/collection-explorer/src/lib/collection-explorer.component.ts index fb1355aa..233f3945 100644 --- a/libs/bulldozer/client/collections/components/collection-explorer/src/lib/collection-explorer.component.ts +++ b/libs/bulldozer/client/collections/components/collection-explorer/src/lib/collection-explorer.component.ts @@ -1,9 +1,15 @@ import { Component, Input } from '@angular/core'; import { + CollectionApiService, CollectionQueryStore, CollectionsStore, } from '@bulldozer-client/collections-data-access'; +import { NotificationStore } from '@bulldozer-client/notifications-data-access'; +import { HdBroadcasterSocketStore } from '@heavy-duty/broadcaster'; +import { CollectionDto } from '@heavy-duty/bulldozer-devkit'; +import { Keypair } from '@solana/web3.js'; import { CollectionExplorerStore } from './collection-explorer.store'; +import { CollectionItemView } from './types'; @Component({ selector: 'bd-collection-explorer', @@ -11,101 +17,131 @@ import { CollectionExplorerStore } from './collection-explorer.store';
- Collections - - + Collections + + + + + + +
- {{ collection.document.name }} + {{ collection.name }} - - - - - + + + + + + + +
@@ -113,8 +149,7 @@ import { CollectionExplorerStore } from './collection-explorer.store'; providers: [CollectionsStore, CollectionQueryStore, CollectionExplorerStore], }) export class CollectionExplorerComponent { - @Input() connected = false; - + @Input() disableCreate = false; @Input() set workspaceId(value: string) { this._collectionExplorerStore.setWorkspaceId(value); } @@ -124,48 +159,131 @@ export class CollectionExplorerComponent { readonly workspaceId$ = this._collectionExplorerStore.workspaceId$; readonly applicationId$ = this._collectionExplorerStore.applicationId$; - readonly collections$ = this._collectionsStore.collections$; + readonly collections$ = this._collectionExplorerStore.collections$; constructor( - private readonly _collectionExplorerStore: CollectionExplorerStore, - private readonly _collectionsStore: CollectionsStore + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _notificationStore: NotificationStore, + private readonly _collectionApiService: CollectionApiService, + private readonly _collectionExplorerStore: CollectionExplorerStore ) {} onCreateCollection( + authority: string, workspaceId: string, applicationId: string, - collectionName: string + collectionDto: CollectionDto ) { - this._collectionExplorerStore.createCollection({ - workspaceId, - applicationId, - collectionName, - }); + const collectionKeypair = Keypair.generate(); + + this._collectionApiService + .create(collectionKeypair, { + authority, + workspaceId, + applicationId, + collectionDto, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Create collection request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `applications:${applicationId}:collections`, + `collections:${collectionKeypair.publicKey.toBase58()}`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); } onUpdateCollection( + authority: string, workspaceId: string, applicationId: string, collectionId: string, - collectionName: string + collectionDto: CollectionDto ) { - this._collectionExplorerStore.updateCollection({ - workspaceId, - applicationId, - collectionId, - collectionName, - }); + this._collectionApiService + .update({ + authority, + workspaceId, + applicationId, + collectionDto, + collectionId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Update collection request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `applications:${applicationId}:collections`, + `collections:${collectionId}`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); } onDeleteCollection( + authority: string, workspaceId: string, applicationId: string, collectionId: string ) { - this._collectionExplorerStore.deleteCollection({ - workspaceId, - applicationId, - collectionId, - }); + this._collectionApiService + .delete({ + authority, + workspaceId, + applicationId, + collectionId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Delete collection request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `applications:${applicationId}:collections`, + `collections:${collectionId}`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); + } + + identify(_: number, collection: CollectionItemView) { + return collection.id; } } diff --git a/libs/bulldozer/client/collections/components/collection-explorer/src/lib/collection-explorer.module.ts b/libs/bulldozer/client/collections/components/collection-explorer/src/lib/collection-explorer.module.ts index 3e095256..a2d79f0a 100644 --- a/libs/bulldozer/client/collections/components/collection-explorer/src/lib/collection-explorer.module.ts +++ b/libs/bulldozer/client/collections/components/collection-explorer/src/lib/collection-explorer.module.ts @@ -11,6 +11,7 @@ import { RouterModule } from '@angular/router'; import { EditCollectionModule } from '@bulldozer-client/edit-collection'; import { ItemUpdatingModule } from '@bulldozer-client/item-updating'; import { StopPropagationModule } from '@bulldozer-client/stop-propagation'; +import { HdWalletAdapterCdkModule } from '@heavy-duty/wallet-adapter-cdk'; import { ReactiveComponentModule } from '@ngrx/component'; import { CollectionExplorerComponent } from './collection-explorer.component'; @@ -26,6 +27,7 @@ import { CollectionExplorerComponent } from './collection-explorer.component'; MatProgressSpinnerModule, MatTooltipModule, ReactiveComponentModule, + HdWalletAdapterCdkModule, StopPropagationModule, EditCollectionModule, ItemUpdatingModule, diff --git a/libs/bulldozer/client/collections/components/collection-explorer/src/lib/collection-explorer.store.ts b/libs/bulldozer/client/collections/components/collection-explorer/src/lib/collection-explorer.store.ts index e1af290b..e4320d08 100644 --- a/libs/bulldozer/client/collections/components/collection-explorer/src/lib/collection-explorer.store.ts +++ b/libs/bulldozer/client/collections/components/collection-explorer/src/lib/collection-explorer.store.ts @@ -1,78 +1,127 @@ import { Injectable } from '@angular/core'; +import { CollectionsStore } from '@bulldozer-client/collections-data-access'; import { - CollectionApiService, - CollectionQueryStore, - CollectionsStore, -} from '@bulldozer-client/collections-data-access'; -import { NotificationStore } from '@bulldozer-client/notifications-data-access'; -import { InstructionStatus } from '@bulldozer-client/users-data-access'; -import { WorkspaceInstructionsStore } from '@bulldozer-client/workspaces-data-access'; -import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; -import { WalletStore } from '@heavy-duty/wallet-adapter'; -import { ComponentStore, tapResponse } from '@ngrx/component-store'; + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; import { - combineLatest, - concatMap, - EMPTY, - filter, - of, - pipe, - switchMap, - tap, - withLatestFrom, -} from 'rxjs'; + Collection, + Document, + flattenInstructions, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined, isTruthy } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-instructions'; +import { CollectionItemView } from './types'; + +const documentToView = (document: Document): CollectionItemView => { + return { + id: document.id, + name: document.name, + isCreating: false, + isUpdating: false, + isDeleting: false, + applicationId: document.data.application, + workspaceId: document.data.workspace, + }; +}; interface ViewModel { - applicationId: string | null; workspaceId: string | null; + applicationId: string | null; + transactions: List; } const initialState: ViewModel = { - applicationId: null, workspaceId: null, + applicationId: null, + transactions: List(), }; @Injectable() export class CollectionExplorerStore extends ComponentStore { readonly workspaceId$ = this.select(({ workspaceId }) => workspaceId); readonly applicationId$ = this.select(({ applicationId }) => applicationId); + private readonly _topicName$ = this.select( + this.applicationId$.pipe(isNotNullOrUndefined), + (applicationId) => `applications:${applicationId}:collections` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly collections$ = this.select( + this._collectionsStore.collections$, + this._instructionStatuses$, + (collections, instructionStatuses) => { + if (collections === null) { + return null; + } + + return instructionStatuses.reduce( + reduceInstructions, + collections.map(documentToView) + ); + }, + { debounce: true } + ); constructor( - private readonly _collectionApiService: CollectionApiService, - private readonly _collectionQueryStore: CollectionQueryStore, - private readonly _collectionsStore: CollectionsStore, - private readonly _notificationStore: NotificationStore, - private readonly _walletStore: WalletStore, - workspaceInstructionsStore: WorkspaceInstructionsStore + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _collectionsStore: CollectionsStore ) { super(initialState); - this._collectionQueryStore.setFilters( - combineLatest({ - application: this.applicationId$.pipe(isNotNullOrUndefined), - }) - ); - this._collectionsStore.setCollectionIds( - this._collectionQueryStore.collectionIds$ + this._collectionsStore.setFilters( + this.select( + this.applicationId$.pipe(isNotNullOrUndefined), + this._hdBroadcasterSocketStore.connected$.pipe(isTruthy), + (applicationId) => ({ application: applicationId }) + ) ); - this._handleInstruction( - this.applicationId$.pipe( - isNotNullOrUndefined, - switchMap((applicationId) => - workspaceInstructionsStore.instruction$.pipe( - filter((instruction) => - instruction.accounts.some( - (account) => - account.name === 'Application' && - account.pubkey === applicationId - ) - ) - ) - ) + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) ) ); } + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) + ); + + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) + ); + readonly setWorkspaceId = this.updater( (state, workspaceId) => ({ ...state, workspaceId }) ); @@ -81,129 +130,69 @@ export class CollectionExplorerStore extends ComponentStore { (state, applicationId) => ({ ...state, applicationId }) ); - private readonly _handleInstruction = this.effect( - tap((instructionStatus) => { - switch (instructionStatus.name) { - case 'createCollection': - case 'updateCollection': - case 'deleteCollection': { - this._collectionsStore.dispatch(instructionStatus); - break; - } - default: - break; + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); } }) ); - readonly createCollection = this.effect<{ - workspaceId: string; - applicationId: string; - collectionName: string; + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap( - ([{ collectionName, workspaceId, applicationId }, authority]) => { - if (authority === null) { - return EMPTY; - } + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } - return this._collectionApiService - .create({ - collectionName, - authority: authority.toBase58(), - workspaceId, - applicationId, - }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent( - 'Create collection request sent' - ), - (error) => this._notificationStore.setError(error) - ) - ); - } - ) - ) - ); + this.patchState({ transactions: List() }); - readonly updateCollection = this.effect<{ - workspaceId: string; - applicationId: string; - collectionId: string; - collectionName: string; - }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap( - ([ - { workspaceId, applicationId, collectionId, collectionName }, - authority, - ]) => { - if (authority === null) { - return EMPTY; - } + const correlationId = uuid(); + let subscriptionId: string; - return this._collectionApiService - .update({ - authority: authority.toBase58(), - workspaceId, - applicationId, - collectionName, - collectionId, - }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent( - 'Update collection request sent' - ), - (error) => this._notificationStore.setError(error) - ) - ); - } - ) - ) - ); + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } - readonly deleteCollection = this.effect<{ - workspaceId: string; - applicationId: string; - collectionId: string; - }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap(([{ workspaceId, collectionId, applicationId }, authority]) => { - if (authority === null) { - return EMPTY; - } - - return this._collectionApiService - .delete({ - authority: authority.toBase58(), - workspaceId, - collectionId, - applicationId, + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName + ); + } + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent( - 'Delete collection request sent' - ), - (error) => this._notificationStore.setError(error) - ) - ); - }) - ) + ); + }) ); } diff --git a/libs/bulldozer/client/collections/components/collection-explorer/src/lib/reduce-instructions.ts b/libs/bulldozer/client/collections/components/collection-explorer/src/lib/reduce-instructions.ts new file mode 100644 index 00000000..3336496e --- /dev/null +++ b/libs/bulldozer/client/collections/components/collection-explorer/src/lib/reduce-instructions.ts @@ -0,0 +1,162 @@ +import { CollectionDto, InstructionStatus } from '@heavy-duty/bulldozer-devkit'; +import { List } from 'immutable'; +import { CollectionItemView } from './types'; + +export const reduceInstructions = ( + items: List, + instruction: InstructionStatus +): List => { + switch (instruction.name) { + case 'createCollection': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: CollectionDto; + }; + const name = data.arguments.name; + + const workspaceId = instruction.accounts.find( + (account) => account.name === 'Workspace' + )?.pubkey; + const applicationId = instruction.accounts.find( + (account) => account.name === 'Application' + )?.pubkey; + const instructionId = instruction.accounts.find( + (account) => account.name === 'Collection' + )?.pubkey; + + if ( + workspaceId === undefined || + applicationId === undefined || + instructionId === undefined + ) { + throw new Error('Malformed Create Collection'); + } + + const itemIndex = items.findIndex((item) => item.id === instructionId); + + if (itemIndex === -1) { + return items.push({ + id: instructionId, + name, + isCreating: true, + isUpdating: false, + isDeleting: false, + applicationId, + workspaceId, + }); + } else { + return items.update(itemIndex, (item) => ({ + ...item, + id: instructionId, + name, + isCreating: true, + isUpdating: false, + isDeleting: false, + applicationId, + workspaceId, + })); + } + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Collection' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isCreating: false, + }; + }); + } else { + return items; + } + } + case 'updateCollection': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: { name: string }; + }; + const name = data.arguments.name; + + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Collection' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + name, + isUpdating: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Collection' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isUpdating: false, + }; + }); + } else { + return items; + } + } + case 'deleteCollection': + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Collection' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isDeleting: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.filter( + (item) => + !instruction.accounts.some( + (account) => + account.name === 'Collection' && account.pubkey === item.id + ) + ); + } else { + return items; + } + default: + return items; + } +}; diff --git a/libs/bulldozer/client/collections/components/collection-explorer/src/lib/types.ts b/libs/bulldozer/client/collections/components/collection-explorer/src/lib/types.ts new file mode 100644 index 00000000..64ef18a8 --- /dev/null +++ b/libs/bulldozer/client/collections/components/collection-explorer/src/lib/types.ts @@ -0,0 +1,9 @@ +export interface CollectionItemView { + id: string; + name: string; + isCreating: boolean; + isUpdating: boolean; + isDeleting: boolean; + applicationId: string; + workspaceId: string; +} diff --git a/libs/bulldozer/client/collections/components/collection-tab/.browserslistrc b/libs/bulldozer/client/collections/components/collection-tab/.browserslistrc deleted file mode 100644 index 4f9ac269..00000000 --- a/libs/bulldozer/client/collections/components/collection-tab/.browserslistrc +++ /dev/null @@ -1,16 +0,0 @@ -# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. -# For additional information regarding the format and rule options, please see: -# https://github.com/browserslist/browserslist#queries - -# For the full list of supported browsers by the Angular framework, please see: -# https://angular.io/guide/browser-support - -# You can see what browsers were selected by your queries by running: -# npx browserslist - -last 1 Chrome version -last 1 Firefox version -last 2 Edge major versions -last 2 Safari major versions -last 2 iOS major versions -Firefox ESR diff --git a/libs/bulldozer/client/collections/components/collection-tab/README.md b/libs/bulldozer/client/collections/components/collection-tab/README.md deleted file mode 100644 index 3297b6c5..00000000 --- a/libs/bulldozer/client/collections/components/collection-tab/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# bulldozer-client-collections-components-collection-tab - -This library was generated with [Nx](https://nx.dev). - -## Running unit tests - -Run `nx test bulldozer-client-collections-components-collection-tab` to execute the unit tests. diff --git a/libs/bulldozer/client/collections/components/collection-tab/project.json b/libs/bulldozer/client/collections/components/collection-tab/project.json deleted file mode 100644 index 08f592e6..00000000 --- a/libs/bulldozer/client/collections/components/collection-tab/project.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "projectType": "library", - "root": "libs/bulldozer/client/collections/components/collection-tab", - "sourceRoot": "libs/bulldozer/client/collections/components/collection-tab/src", - "prefix": "bd", - "targets": { - "test": { - "executor": "@nrwl/jest:jest", - "outputs": [ - "coverage/libs/bulldozer/client/collections/components/collection-tab" - ], - "options": { - "jestConfig": "libs/bulldozer/client/collections/components/collection-tab/jest.config.js", - "passWithNoTests": true - } - }, - "lint": { - "executor": "@nrwl/linter:eslint", - "options": { - "lintFilePatterns": [ - "libs/bulldozer/client/collections/components/collection-tab/src/**/*.ts", - "libs/bulldozer/client/collections/components/collection-tab/src/**/*.html" - ] - } - } - }, - "tags": [] -} diff --git a/libs/bulldozer/client/collections/components/collection-tab/src/index.ts b/libs/bulldozer/client/collections/components/collection-tab/src/index.ts deleted file mode 100644 index ed760f05..00000000 --- a/libs/bulldozer/client/collections/components/collection-tab/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './lib/collection-tab.module'; diff --git a/libs/bulldozer/client/collections/components/collection-tab/src/lib/collection-tab.component.ts b/libs/bulldozer/client/collections/components/collection-tab/src/lib/collection-tab.component.ts deleted file mode 100644 index 9d197bd5..00000000 --- a/libs/bulldozer/client/collections/components/collection-tab/src/lib/collection-tab.component.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Component, HostBinding, Input } from '@angular/core'; -import { CollectionStore } from '@bulldozer-client/collections-data-access'; -import { TabStore } from '@bulldozer-client/core-data-access'; -import { CollectionTabStore } from './collection-tab.store'; - -@Component({ - selector: 'bd-collection-tab', - template: ` - - `, - providers: [CollectionStore, CollectionTabStore], -}) -export class CollectionTabComponent { - @HostBinding('class') class = 'block w-full'; - - @Input() set collectionId(value: string) { - this._collectionTabStore.setCollectionId(value); - } - - readonly collection$ = this._collectionStore.collection$; - - constructor( - private readonly _tabStore: TabStore, - private readonly _collectionTabStore: CollectionTabStore, - private readonly _collectionStore: CollectionStore - ) {} - - onCloseTab(collectionId: string) { - this._tabStore.closeTab(collectionId); - } -} diff --git a/libs/bulldozer/client/collections/components/collection-tab/src/lib/collection-tab.store.ts b/libs/bulldozer/client/collections/components/collection-tab/src/lib/collection-tab.store.ts deleted file mode 100644 index e520fef3..00000000 --- a/libs/bulldozer/client/collections/components/collection-tab/src/lib/collection-tab.store.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { Injectable } from '@angular/core'; -import { CollectionStore } from '@bulldozer-client/collections-data-access'; -import { TabStore } from '@bulldozer-client/core-data-access'; -import { InstructionStatus } from '@bulldozer-client/users-data-access'; -import { WorkspaceInstructionsStore } from '@bulldozer-client/workspaces-data-access'; -import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; -import { ComponentStore } from '@ngrx/component-store'; -import { filter, switchMap, tap } from 'rxjs'; - -interface ViewModel { - collectionId: string | null; -} - -const initialState: ViewModel = { - collectionId: null, -}; - -@Injectable() -export class CollectionTabStore extends ComponentStore { - readonly collectionId$ = this.select(({ collectionId }) => collectionId); - - constructor( - private readonly _tabStore: TabStore, - private readonly _collectionStore: CollectionStore, - workspaceInstructionsStore: WorkspaceInstructionsStore - ) { - super(initialState); - - this._collectionStore.setCollectionId(this.collectionId$); - this._handleInstruction( - this.collectionId$.pipe( - isNotNullOrUndefined, - switchMap((collectionId) => - workspaceInstructionsStore.instruction$.pipe( - filter((instruction) => - instruction.accounts.some( - (account) => - account.name === 'Collection' && - account.pubkey === collectionId - ) - ) - ) - ) - ) - ); - this._handleCollectionDeleted( - this.select( - this.collectionId$.pipe(isNotNullOrUndefined), - workspaceInstructionsStore.instruction$.pipe( - filter( - (instruction) => - instruction.name === 'deleteCollection' && - instruction.status === 'finalized' - ) - ), - (collectionId, instructionStatus) => ({ - collectionId, - instructionStatus, - }) - ).pipe( - filter(({ collectionId, instructionStatus }) => - instructionStatus.accounts.some( - (account) => - account.name === 'Collection' && account.pubkey === collectionId - ) - ) - ) - ); - } - - readonly setCollectionId = this.updater( - (state, collectionId) => ({ ...state, collectionId }) - ); - - private readonly _handleInstruction = this.effect( - tap((instructionStatus) => { - switch (instructionStatus.name) { - case 'createCollection': - case 'updateCollection': - case 'deleteCollection': { - this._collectionStore.dispatch(instructionStatus); - break; - } - default: - break; - } - }) - ); - - private readonly _handleCollectionDeleted = this.effect<{ - collectionId: string; - instructionStatus: InstructionStatus; - }>(tap(({ collectionId }) => this._tabStore.closeTab(collectionId))); -} diff --git a/libs/bulldozer/client/collections/data-access/src/lib/collection-api.service.ts b/libs/bulldozer/client/collections/data-access/src/lib/collection-api.service.ts index bf402d3e..64b6ca7a 100644 --- a/libs/bulldozer/client/collections/data-access/src/lib/collection-api.service.ts +++ b/libs/bulldozer/client/collections/data-access/src/lib/collection-api.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@angular/core'; -import { HdBroadcasterStore } from '@heavy-duty/broadcaster'; import { BULLDOZER_PROGRAM_ID, Collection, @@ -30,7 +29,6 @@ import { first, map, Observable, - tap, throwError, } from 'rxjs'; @@ -38,8 +36,7 @@ import { export class CollectionApiService { constructor( private readonly _hdSolanaApiService: HdSolanaApiService, - private readonly _hdSolanaConfigStore: HdSolanaConfigStore, - private readonly _hdBroadcasterStore: HdBroadcasterStore + private readonly _hdSolanaConfigStore: HdSolanaConfigStore ) {} private handleError(error: string) { @@ -64,8 +61,26 @@ export class CollectionApiService { ); } + // get collection + findById( + collectionId: string, + commitment: Finality = 'finalized' + ): Observable | null> { + return this._hdSolanaApiService + .getAccountInfo(collectionId, commitment) + .pipe( + map( + (accountInfo) => + accountInfo && createCollectionDocument(collectionId, accountInfo) + ) + ); + } + // get collections - findByIds(collectionIds: string[], commitment: Finality = 'finalized') { + findByIds( + collectionIds: string[], + commitment: Finality = 'finalized' + ): Observable<(Document | null)[]> { return this._hdSolanaApiService .getMultipleAccounts(collectionIds, { commitment }) .pipe( @@ -82,25 +97,11 @@ export class CollectionApiService { ); } - // get collection - findById( - collectionId: string, - commitment: Finality = 'finalized' - ): Observable | null> { - return this._hdSolanaApiService - .getAccountInfo(collectionId, commitment) - .pipe( - map( - (accountInfo) => - accountInfo && createCollectionDocument(collectionId, accountInfo) - ) - ); - } - // create collection - create(params: Omit) { - const collectionKeypair = Keypair.generate(); - + create( + collectionKeypair: Keypair, + params: Omit + ) { return this._hdSolanaApiService.createTransaction(params.authority).pipe( addInstructionToTransaction( this._hdSolanaConfigStore.apiEndpoint$.pipe( @@ -120,12 +121,10 @@ export class CollectionApiService { partiallySignTransaction(collectionKeypair), concatMap((transaction) => this._hdSolanaApiService.sendTransaction(transaction).pipe( - tap((transactionSignature) => - this._hdBroadcasterStore.sendTransaction( - transactionSignature, - params.workspaceId - ) - ), + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), catchError((error) => this.handleError(error)) ) ) @@ -149,12 +148,10 @@ export class CollectionApiService { ), concatMap((transaction) => this._hdSolanaApiService.sendTransaction(transaction).pipe( - tap((transactionSignature) => - this._hdBroadcasterStore.sendTransaction( - transactionSignature, - params.workspaceId - ) - ), + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), catchError((error) => this.handleError(error)) ) ) @@ -178,12 +175,10 @@ export class CollectionApiService { ), concatMap((transaction) => this._hdSolanaApiService.sendTransaction(transaction).pipe( - tap((transactionSignature) => - this._hdBroadcasterStore.sendTransaction( - transactionSignature, - params.workspaceId - ) - ), + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), catchError((error) => this.handleError(error)) ) ) diff --git a/libs/bulldozer/client/collections/data-access/src/lib/collection-attribute-api.service.ts b/libs/bulldozer/client/collections/data-access/src/lib/collection-attribute-api.service.ts index e51f50bd..e4058abd 100644 --- a/libs/bulldozer/client/collections/data-access/src/lib/collection-attribute-api.service.ts +++ b/libs/bulldozer/client/collections/data-access/src/lib/collection-attribute-api.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@angular/core'; -import { HdBroadcasterStore } from '@heavy-duty/broadcaster'; import { BULLDOZER_PROGRAM_ID, CollectionAttribute, @@ -23,14 +22,18 @@ import { addInstructionToTransaction, partiallySignTransaction, } from '@heavy-duty/rx-solana'; -import { Finality, Keypair } from '@solana/web3.js'; +import { + Finality, + Keypair, + Transaction, + TransactionSignature, +} from '@solana/web3.js'; import { catchError, concatMap, first, map, Observable, - tap, throwError, } from 'rxjs'; @@ -38,8 +41,7 @@ import { export class CollectionAttributeApiService { constructor( private readonly _hdSolanaApiService: HdSolanaApiService, - private readonly _hdSolanaConfigStore: HdSolanaConfigStore, - private readonly _hdBroadcasterStore: HdBroadcasterStore + private readonly _hdSolanaConfigStore: HdSolanaConfigStore ) {} private handleError(error: string) { @@ -56,14 +58,36 @@ export class CollectionAttributeApiService { return this._hdSolanaApiService .getProgramAccounts(BULLDOZER_PROGRAM_ID.toBase58(), { ...query, + dataSlice: { + offset: 0, + length: 0, + }, commitment, - dataSlice: { offset: 0, length: 0 }, }) .pipe( map((programAccounts) => programAccounts.map(({ pubkey }) => pubkey)) ); } + // get collection attribute + findById( + collectionAttributeId: string, + commitment: Finality = 'finalized' + ): Observable | null> { + return this._hdSolanaApiService + .getAccountInfo(collectionAttributeId, commitment) + .pipe( + map( + (accountInfo) => + accountInfo && + createCollectionAttributeDocument( + collectionAttributeId, + accountInfo + ) + ) + ); + } + // get collection attributes findByIds( collectionAttributeIds: string[], @@ -85,31 +109,14 @@ export class CollectionAttributeApiService { ); } - // get collection attribute - findById( - collectionAttributeId: string, - commitment: Finality = 'finalized' - ): Observable | null> { - return this._hdSolanaApiService - .getAccountInfo(collectionAttributeId, commitment) - .pipe( - map( - (accountInfo) => - accountInfo && - createCollectionAttributeDocument( - collectionAttributeId, - accountInfo - ) - ) - ); - } - // create collection attribute create( + collectionAttributeKeypair: Keypair, params: Omit - ) { - const collectionAttributeKeypair = Keypair.generate(); - + ): Observable<{ + transactionSignature: TransactionSignature; + transaction: Transaction; + }> { return this._hdSolanaApiService.createTransaction(params.authority).pipe( addInstructionToTransaction( this._hdSolanaConfigStore.apiEndpoint$.pipe( @@ -130,12 +137,10 @@ export class CollectionAttributeApiService { partiallySignTransaction(collectionAttributeKeypair), concatMap((transaction) => this._hdSolanaApiService.sendTransaction(transaction).pipe( - tap((transactionSignature) => - this._hdBroadcasterStore.sendTransaction( - transactionSignature, - params.workspaceId - ) - ), + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), catchError((error) => this.handleError(error)) ) ) @@ -143,7 +148,10 @@ export class CollectionAttributeApiService { } // update collection attribute - update(params: UpdateCollectionAttributeParams) { + update(params: UpdateCollectionAttributeParams): Observable<{ + transactionSignature: TransactionSignature; + transaction: Transaction; + }> { return this._hdSolanaApiService.createTransaction(params.authority).pipe( addInstructionToTransaction( this._hdSolanaConfigStore.apiEndpoint$.pipe( @@ -159,12 +167,10 @@ export class CollectionAttributeApiService { ), concatMap((transaction) => this._hdSolanaApiService.sendTransaction(transaction).pipe( - tap((transactionSignature) => - this._hdBroadcasterStore.sendTransaction( - transactionSignature, - params.workspaceId - ) - ), + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), catchError((error) => this.handleError(error)) ) ) @@ -172,7 +178,10 @@ export class CollectionAttributeApiService { } // delete collection attribute - delete(params: DeleteCollectionAttributeParams) { + delete(params: DeleteCollectionAttributeParams): Observable<{ + transactionSignature: TransactionSignature; + transaction: Transaction; + }> { return this._hdSolanaApiService.createTransaction(params.authority).pipe( addInstructionToTransaction( this._hdSolanaConfigStore.apiEndpoint$.pipe( @@ -188,12 +197,10 @@ export class CollectionAttributeApiService { ), concatMap((transaction) => this._hdSolanaApiService.sendTransaction(transaction).pipe( - tap((transactionSignature) => - this._hdBroadcasterStore.sendTransaction( - transactionSignature, - params.workspaceId - ) - ), + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), catchError((error) => this.handleError(error)) ) ) diff --git a/libs/bulldozer/client/collections/data-access/src/lib/collection-attributes.store.ts b/libs/bulldozer/client/collections/data-access/src/lib/collection-attributes.store.ts index 6b923d6f..2e65d676 100644 --- a/libs/bulldozer/client/collections/data-access/src/lib/collection-attributes.store.ts +++ b/libs/bulldozer/client/collections/data-access/src/lib/collection-attributes.store.ts @@ -1,32 +1,33 @@ import { Injectable } from '@angular/core'; import { NotificationStore } from '@bulldozer-client/notifications-data-access'; -import { InstructionStatus } from '@bulldozer-client/users-data-access'; -import { CollectionAttribute, Document } from '@heavy-duty/bulldozer-devkit'; -import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { + CollectionAttribute, + CollectionAttributeFilters, + Document, +} from '@heavy-duty/bulldozer-devkit'; import { ComponentStore, tapResponse } from '@ngrx/component-store'; -import { concatMap, EMPTY, switchMap } from 'rxjs'; +import { List, Map } from 'immutable'; +import { EMPTY, switchMap } from 'rxjs'; import { CollectionAttributeApiService } from './collection-attribute-api.service'; -import { ItemView } from './types'; - -export type CollectionAttributeItemView = ItemView< - Document ->; interface ViewModel { loading: boolean; - collectionAttributeIds: string[] | null; - collectionAttributesMap: Map; + filters: CollectionAttributeFilters | null; + collectionAttributeIds: List | null; + collectionAttributesMap: Map> | null; } const initialState: ViewModel = { loading: false, + filters: null, collectionAttributeIds: null, - collectionAttributesMap: new Map(), + collectionAttributesMap: null, }; @Injectable() export class CollectionAttributesStore extends ComponentStore { readonly loading$ = this.select(({ loading }) => loading); + readonly filters$ = this.select(({ filters }) => filters); readonly collectionAttributeIds$ = this.select( ({ collectionAttributeIds }) => collectionAttributeIds ); @@ -36,10 +37,11 @@ export class CollectionAttributesStore extends ComponentStore { readonly collectionAttributes$ = this.select( this.collectionAttributesMap$, (collectionAttributesMap) => - Array.from( - collectionAttributesMap, - ([, collectionAttribute]) => collectionAttribute - ) + collectionAttributesMap === null + ? null + : collectionAttributesMap + .toList() + .sort((a, b) => (b.createdAt.lt(a.createdAt) ? 1 : -1)) ); constructor( @@ -49,81 +51,51 @@ export class CollectionAttributesStore extends ComponentStore { super(initialState); this._loadCollectionAttributes(this.collectionAttributeIds$); + this._loadCollectionAttributeIds(this.filters$); } - private readonly _setCollectionAttribute = - this.updater( - (state, newCollectionAttribute) => { - const collectionAttributesMap = new Map(state.collectionAttributesMap); - collectionAttributesMap.set( - newCollectionAttribute.document.id, - newCollectionAttribute - ); - - return { - ...state, - collectionAttributesMap, - }; - } - ); - - private readonly _patchStatus = this.updater<{ - collectionAttributeId: string; - statuses: { - isCreating?: boolean; - isUpdating?: boolean; - isDeleting?: boolean; - }; - }>((state, { collectionAttributeId, statuses }) => { - const collectionAttributesMap = new Map(state.collectionAttributesMap); - const collectionAttribute = collectionAttributesMap.get( - collectionAttributeId - ); - - if (collectionAttribute === undefined) { - return state; - } - - return { + readonly setFilters = this.updater( + (state, filters) => ({ ...state, - collectionAttributesMap: collectionAttributesMap.set( - collectionAttributeId, - { - ...collectionAttribute, - ...statuses, + filters, + collectionAttributeIds: null, + collectionAttributesMap: null, + }) + ); + + private readonly _loadCollectionAttributeIds = + this.effect( + switchMap((filters) => { + if (filters === null) { + return EMPTY; } - ), - }; - }); - private readonly _removeCollectionAttribute = this.updater( - (state, collectionAttributeId) => { - const collectionAttributesMap = new Map(state.collectionAttributesMap); - collectionAttributesMap.delete(collectionAttributeId); - return { - ...state, - collectionAttributesMap, - }; - } - ); + this.patchState({ + loading: true, + collectionAttributesMap: null, + }); - readonly setCollectionAttributeIds = this.updater( - (state, collectionAttributeIds) => ({ - ...state, - collectionAttributeIds, - }) - ); + return this._collectionAttributeApiService.findIds(filters).pipe( + tapResponse( + (collectionAttributeIds) => { + this.patchState({ + collectionAttributeIds: List(collectionAttributeIds), + }); + }, + (error) => this._notificationStore.setError(error) + ) + ); + }) + ); - private readonly _loadCollectionAttributes = this.effect( + private readonly _loadCollectionAttributes = this.effect | null>( switchMap((collectionAttributeIds) => { if (collectionAttributeIds === null) { return EMPTY; } - this.patchState({ loading: true }); - return this._collectionAttributeApiService - .findByIds(collectionAttributeIds) + .findByIds(collectionAttributeIds.toArray()) .pipe( tapResponse( (collectionAttributes) => { @@ -138,13 +110,11 @@ export class CollectionAttributesStore extends ComponentStore { ) .reduce( (collectionAttributesMap, collectionAttribute) => - collectionAttributesMap.set(collectionAttribute.id, { - document: collectionAttribute, - isCreating: false, - isUpdating: false, - isDeleting: false, - }), - new Map() + collectionAttributesMap.set( + collectionAttribute.id, + collectionAttribute + ), + Map>() ), }); }, @@ -153,91 +123,4 @@ export class CollectionAttributesStore extends ComponentStore { ); }) ); - - readonly dispatch = this.effect( - concatMap((instructionStatus) => { - const collectionAttributeAccountMeta = instructionStatus.accounts.find( - (account) => account.name === 'Attribute' - ); - - if (collectionAttributeAccountMeta === undefined) { - return EMPTY; - } - - switch (instructionStatus.name) { - case 'createCollectionAttribute': { - if (instructionStatus.status === 'finalized') { - this._patchStatus({ - collectionAttributeId: collectionAttributeAccountMeta.pubkey, - statuses: { - isCreating: false, - }, - }); - - return EMPTY; - } - - return this._collectionAttributeApiService - .findById(collectionAttributeAccountMeta.pubkey, 'confirmed') - .pipe( - isNotNullOrUndefined, - tapResponse( - (collectionAttribute) => - this._setCollectionAttribute({ - document: collectionAttribute, - isCreating: true, - isUpdating: false, - isDeleting: false, - }), - (error) => this._notificationStore.setError({ error }) - ) - ); - } - case 'updateCollectionAttribute': { - if (instructionStatus.status === 'finalized') { - this._patchStatus({ - collectionAttributeId: collectionAttributeAccountMeta.pubkey, - statuses: { - isUpdating: false, - }, - }); - - return EMPTY; - } - - return this._collectionAttributeApiService - .findById(collectionAttributeAccountMeta.pubkey, 'confirmed') - .pipe( - isNotNullOrUndefined, - tapResponse( - (collectionAttribute) => - this._setCollectionAttribute({ - document: collectionAttribute, - isCreating: false, - isUpdating: true, - isDeleting: false, - }), - (error) => this._notificationStore.setError({ error }) - ) - ); - } - case 'deleteCollectionAttribute': { - if (instructionStatus.status === 'confirmed') { - this._patchStatus({ - collectionAttributeId: collectionAttributeAccountMeta.pubkey, - statuses: { isDeleting: true }, - }); - } else { - this._removeCollectionAttribute( - collectionAttributeAccountMeta.pubkey - ); - } - - return EMPTY; - } - default: - return EMPTY; - } - }) - ); } diff --git a/libs/bulldozer/client/collections/data-access/src/lib/collection.store.ts b/libs/bulldozer/client/collections/data-access/src/lib/collection.store.ts index 202dd110..8bd79f68 100644 --- a/libs/bulldozer/client/collections/data-access/src/lib/collection.store.ts +++ b/libs/bulldozer/client/collections/data-access/src/lib/collection.store.ts @@ -1,40 +1,27 @@ import { Injectable } from '@angular/core'; import { NotificationStore } from '@bulldozer-client/notifications-data-access'; -import { InstructionStatus } from '@bulldozer-client/users-data-access'; import { Collection, Document } from '@heavy-duty/bulldozer-devkit'; import { ComponentStore, tapResponse } from '@ngrx/component-store'; -import { - BehaviorSubject, - combineLatest, - concatMap, - EMPTY, - map, - switchMap, -} from 'rxjs'; +import { EMPTY, switchMap } from 'rxjs'; import { CollectionApiService } from './collection-api.service'; -import { ItemView } from './types'; - -export type CollectionView = ItemView>; interface ViewModel { - collectionId: string | null; - collection: CollectionView | null; loading: boolean; + collectionId: string | null; + collection: Document | null; } const initialState: ViewModel = { + loading: false, collectionId: null, collection: null, - loading: false, }; @Injectable() export class CollectionStore extends ComponentStore { - private readonly _reload = new BehaviorSubject(null); - private readonly reload$ = this._reload.asObservable(); - readonly collection$ = this.select(({ collection }) => collection); - readonly collectionId$ = this.select(({ collectionId }) => collectionId); readonly loading$ = this.select(({ loading }) => loading); + readonly collectionId$ = this.select(({ collectionId }) => collectionId); + readonly collection$ = this.select(({ collection }) => collection); constructor( private readonly _collectionApiService: CollectionApiService, @@ -42,11 +29,7 @@ export class CollectionStore extends ComponentStore { ) { super(initialState); - this._loadCollection( - combineLatest([this.collectionId$, this.reload$]).pipe( - map(([collectionId]) => collectionId) - ) - ); + this._loadCollection(this.collectionId$); } readonly setCollectionId = this.updater( @@ -56,131 +39,25 @@ export class CollectionStore extends ComponentStore { }) ); - private readonly _patchStatus = this.updater<{ - isCreating?: boolean; - isUpdating?: boolean; - isDeleting?: boolean; - }>((state, statuses) => ({ - ...state, - collection: state.collection - ? { - ...state.collection, - ...statuses, - } - : null, - })); - - private readonly _setCollection = this.updater( - (state, collection) => ({ - ...state, - collection, - }) - ); - private readonly _loadCollection = this.effect( switchMap((collectionId) => { if (collectionId === null) { - this.patchState({ collection: null }); return EMPTY; } - this.patchState({ loading: true }); + this.patchState({ loading: true, collection: null }); return this._collectionApiService.findById(collectionId).pipe( tapResponse( (collection) => { - if (collection !== null) { - this._setCollection({ - document: collection, - isCreating: false, - isUpdating: false, - isDeleting: false, - }); - } - this.patchState({ loading: false }); + this.patchState({ + loading: false, + collection, + }); }, - (error) => this._notificationStore.setError({ error, loading: false }) + (error) => this._notificationStore.setError({ error }) ) ); }) ); - - readonly dispatch = this.effect( - concatMap((instructionStatus) => { - const collectionAccountMeta = instructionStatus.accounts.find( - (account) => account.name === 'Collection' - ); - - if (collectionAccountMeta === undefined) { - return EMPTY; - } - - switch (instructionStatus.name) { - case 'createCollection': { - if (instructionStatus.status === 'finalized') { - this._patchStatus({ isCreating: false }); - return EMPTY; - } - - return this._collectionApiService - .findById(collectionAccountMeta.pubkey, 'confirmed') - .pipe( - tapResponse( - (collection) => { - if (collection !== null) { - this._setCollection({ - document: collection, - isCreating: true, - isUpdating: false, - isDeleting: false, - }); - } - }, - (error) => this._notificationStore.setError({ error }) - ) - ); - } - case 'updateCollection': { - if (instructionStatus.status === 'finalized') { - this._patchStatus({ isUpdating: false }); - return EMPTY; - } - - return this._collectionApiService - .findById(collectionAccountMeta.pubkey, 'confirmed') - .pipe( - tapResponse( - (collection) => { - if (collection !== null) { - this._setCollection({ - document: collection, - isCreating: false, - isUpdating: true, - isDeleting: false, - }); - } - }, - (error) => this._notificationStore.setError({ error }) - ) - ); - } - case 'deleteCollection': { - if (instructionStatus.status === 'confirmed') { - this._patchStatus({ isDeleting: true }); - } else { - this.patchState({ collection: null }); - this._patchStatus({ isDeleting: false }); - } - - return EMPTY; - } - default: - return EMPTY; - } - }) - ); - - reload() { - this._reload.next(null); - } } diff --git a/libs/bulldozer/client/collections/data-access/src/lib/collections.store.ts b/libs/bulldozer/client/collections/data-access/src/lib/collections.store.ts index f8b9e92d..5aec4b5c 100644 --- a/libs/bulldozer/client/collections/data-access/src/lib/collections.store.ts +++ b/libs/bulldozer/client/collections/data-access/src/lib/collections.store.ts @@ -1,36 +1,43 @@ import { Injectable } from '@angular/core'; import { NotificationStore } from '@bulldozer-client/notifications-data-access'; -import { InstructionStatus } from '@bulldozer-client/users-data-access'; -import { Collection, Document } from '@heavy-duty/bulldozer-devkit'; -import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { + Collection, + CollectionFilters, + Document, +} from '@heavy-duty/bulldozer-devkit'; import { ComponentStore, tapResponse } from '@ngrx/component-store'; -import { concatMap, EMPTY, switchMap } from 'rxjs'; +import { List, Map } from 'immutable'; +import { EMPTY, switchMap } from 'rxjs'; import { CollectionApiService } from './collection-api.service'; -import { ItemView } from './types'; - -export type CollectionItemView = ItemView>; interface ViewModel { loading: boolean; - collectionIds: string[] | null; - collectionsMap: Map; + filters: CollectionFilters | null; + collectionIds: List | null; + collectionsMap: Map> | null; } const initialState: ViewModel = { loading: false, + filters: null, collectionIds: null, - collectionsMap: new Map(), + collectionsMap: null, }; @Injectable() export class CollectionsStore extends ComponentStore { readonly loading$ = this.select(({ loading }) => loading); + readonly filters$ = this.select(({ filters }) => filters); readonly collectionIds$ = this.select(({ collectionIds }) => collectionIds); readonly collectionsMap$ = this.select( ({ collectionsMap }) => collectionsMap ); readonly collections$ = this.select(this.collectionsMap$, (collectionsMap) => - Array.from(collectionsMap, ([, collection]) => collection) + collectionsMap === null + ? null + : collectionsMap + .toList() + .sort((a, b) => (b.createdAt.lt(a.createdAt) ? 1 : -1)) ); constructor( @@ -40,71 +47,59 @@ export class CollectionsStore extends ComponentStore { super(initialState); this._loadCollections(this.collectionIds$); + this._loadCollectionIds(this.filters$); } - readonly setCollectionIds = this.updater( - (state, collectionIds) => ({ + readonly setFilters = this.updater( + (state, filters) => ({ ...state, - collectionIds, + filters, + collectionIds: null, + collectionsMap: null, }) ); - private readonly _setCollection = this.updater( - (state, newCollection) => { - const collectionsMap = new Map(state.collectionsMap); - collectionsMap.set(newCollection.document.id, newCollection); - - return { - ...state, - collectionsMap, - }; - } - ); - - private readonly _patchStatus = this.updater<{ - collectionId: string; - statuses: { - isCreating?: boolean; - isUpdating?: boolean; - isDeleting?: boolean; - }; - }>((state, { collectionId, statuses }) => { - const collectionsMap = new Map(state.collectionsMap); - const collection = collectionsMap.get(collectionId); - - if (collection === undefined) { - return state; - } + private readonly _loadCollectionIds = this.effect( + switchMap((filters) => { + if (filters === null) { + return EMPTY; + } - return { - ...state, - collectionsMap: collectionsMap.set(collectionId, { - ...collection, - ...statuses, - }), - }; - }); + this.patchState({ + loading: true, + collectionIds: List(), + collectionsMap: null, + }); - private readonly _removeCollection = this.updater( - (state, collectionId) => { - const collectionsMap = new Map(state.collectionsMap); - collectionsMap.delete(collectionId); - return { - ...state, - collectionsMap, - }; - } + return this._collectionApiService.findIds(filters).pipe( + tapResponse( + (collectionIds) => { + this.patchState({ + collectionIds: List(collectionIds), + }); + }, + (error) => this._notificationStore.setError(error) + ) + ); + }) ); - private readonly _loadCollections = this.effect( + private readonly _loadCollections = this.effect | null>( switchMap((collectionIds) => { if (collectionIds === null) { return EMPTY; } - this.patchState({ loading: true }); + if (collectionIds.size === 0) { + this.patchState({ + loading: false, + collectionsMap: Map>(), + }); + + return EMPTY; + } - return this._collectionApiService.findByIds(collectionIds).pipe( + return this._collectionApiService.findByIds(collectionIds.toArray()).pipe( tapResponse( (collections) => { this.patchState({ @@ -116,13 +111,8 @@ export class CollectionsStore extends ComponentStore { ) .reduce( (collectionsMap, collection) => - collectionsMap.set(collection.id, { - document: collection, - isCreating: false, - isUpdating: false, - isDeleting: false, - }), - new Map() + collectionsMap.set(collection.id, collection), + Map>() ), }); }, @@ -131,89 +121,4 @@ export class CollectionsStore extends ComponentStore { ); }) ); - - readonly dispatch = this.effect( - concatMap((instructionStatus) => { - const collectionAccountMeta = instructionStatus.accounts.find( - (account) => account.name === 'Collection' - ); - - if (collectionAccountMeta === undefined) { - return EMPTY; - } - - switch (instructionStatus.name) { - case 'createCollection': { - if (instructionStatus.status === 'finalized') { - this._patchStatus({ - collectionId: collectionAccountMeta.pubkey, - statuses: { - isCreating: false, - }, - }); - - return EMPTY; - } - - return this._collectionApiService - .findById(collectionAccountMeta.pubkey, 'confirmed') - .pipe( - isNotNullOrUndefined, - tapResponse( - (collection) => - this._setCollection({ - document: collection, - isCreating: true, - isUpdating: false, - isDeleting: false, - }), - (error) => this._notificationStore.setError({ error }) - ) - ); - } - case 'updateCollection': { - if (instructionStatus.status === 'finalized') { - this._patchStatus({ - collectionId: collectionAccountMeta.pubkey, - statuses: { - isUpdating: false, - }, - }); - - return EMPTY; - } - - return this._collectionApiService - .findById(collectionAccountMeta.pubkey, 'confirmed') - .pipe( - isNotNullOrUndefined, - tapResponse( - (collection) => - this._setCollection({ - document: collection, - isCreating: false, - isUpdating: true, - isDeleting: false, - }), - (error) => this._notificationStore.setError({ error }) - ) - ); - } - case 'deleteCollection': { - if (instructionStatus.status === 'confirmed') { - this._patchStatus({ - collectionId: collectionAccountMeta.pubkey, - statuses: { isDeleting: true }, - }); - } else { - this._removeCollection(collectionAccountMeta.pubkey); - } - - return EMPTY; - } - default: - return EMPTY; - } - }) - ); } diff --git a/libs/bulldozer/client/collections/modals/edit-collection-attribute/src/lib/edit-collection-attribute.component.ts b/libs/bulldozer/client/collections/modals/edit-collection-attribute/src/lib/edit-collection-attribute.component.ts index b94a253a..3dc7c871 100644 --- a/libs/bulldozer/client/collections/modals/edit-collection-attribute/src/lib/edit-collection-attribute.component.ts +++ b/libs/bulldozer/client/collections/modals/edit-collection-attribute/src/lib/edit-collection-attribute.component.ts @@ -8,14 +8,15 @@ import { import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { CollectionAttribute, Document } from '@heavy-duty/bulldozer-devkit'; +import { SnackBarComponent } from '@bulldozer-client/notification-snack-bar'; +import { CollectionAttributeDto } from '@heavy-duty/bulldozer-devkit'; import { Subject, takeUntil } from 'rxjs'; @Component({ selector: 'bd-edit-attribute', template: ` -

- {{ data?.collectionAttribute ? 'Edit' : 'Create' }} attribute +

+ {{ collectionAttribute ? 'Edit' : 'Create' }} attribute

- + + +
+
+
+
+
+
+
- - `, }) export class EditCollectionAttributeComponent implements OnInit, OnDestroy { @@ -184,29 +186,19 @@ export class EditCollectionAttributeComponent implements OnInit, OnDestroy { private readonly _matSnackBar: MatSnackBar, private readonly _matDialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) - public data?: { collectionAttribute?: Document } + public collectionAttribute?: CollectionAttributeDto ) { this.form = new FormGroup({ - name: new FormControl(this.data?.collectionAttribute?.name ?? '', { + name: new FormControl(this.collectionAttribute?.name ?? '', { validators: [Validators.required], }), - kind: new FormControl(this.data?.collectionAttribute?.data.kind.id ?? 0, { + kind: new FormControl(this.collectionAttribute?.kind ?? 0, { validators: [Validators.required], }), - modifier: new FormControl( - this.data?.collectionAttribute?.data.modifier !== null - ? this.data?.collectionAttribute?.data.modifier.id - : null - ), - size: new FormControl( - this.data?.collectionAttribute?.data.modifier !== null - ? this.data?.collectionAttribute?.data.modifier.size - : null - ), - max: new FormControl(this.data?.collectionAttribute?.data.max ?? null), - maxLength: new FormControl( - this.data?.collectionAttribute?.data.maxLength ?? null - ), + modifier: new FormControl(this.collectionAttribute?.modifier ?? null), + size: new FormControl(this.collectionAttribute?.size ?? null), + max: new FormControl(this.collectionAttribute?.max ?? null), + maxLength: new FormControl(this.collectionAttribute?.maxLength ?? null), }); } @@ -268,9 +260,13 @@ export class EditCollectionAttributeComponent implements OnInit, OnDestroy { if (this.form.valid) { this._matDialogRef.close(this.form.value); } else { - this._matSnackBar.open('Invalid information', 'close', { - panelClass: 'warning-snackbar', + this._matSnackBar.openFromComponent(SnackBarComponent, { duration: 5000, + data: { + title: 'Heey...', + message: 'Invalid Information', + type: 'warning', + }, }); } } diff --git a/libs/bulldozer/client/collections/modals/edit-collection-attribute/src/lib/edit-collection-attribute-trigger.directive.ts b/libs/bulldozer/client/collections/modals/edit-collection-attribute/src/lib/edit-collection-attribute.directive.ts similarity index 62% rename from libs/bulldozer/client/collections/modals/edit-collection-attribute/src/lib/edit-collection-attribute-trigger.directive.ts rename to libs/bulldozer/client/collections/modals/edit-collection-attribute/src/lib/edit-collection-attribute.directive.ts index ce20fbb5..4328862b 100644 --- a/libs/bulldozer/client/collections/modals/edit-collection-attribute/src/lib/edit-collection-attribute-trigger.directive.ts +++ b/libs/bulldozer/client/collections/modals/edit-collection-attribute/src/lib/edit-collection-attribute.directive.ts @@ -6,26 +6,23 @@ import { Output, } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { - CollectionAttribute, - CollectionAttributeDto, - Document, -} from '@heavy-duty/bulldozer-devkit'; +import { CollectionAttributeDto } from '@heavy-duty/bulldozer-devkit'; import { EditCollectionAttributeComponent } from './edit-collection-attribute.component'; -@Directive({ selector: '[bdEditCollectionAttributeTrigger]' }) -export class EditCollectionAttributeTriggerDirective { - @Input() collectionAttribute?: Document; +@Directive({ selector: '[bdEditCollectionAttribute]' }) +export class EditCollectionAttributeDirective { + @Input() collectionAttribute?: CollectionAttributeDto; @Output() editCollectionAttribute = new EventEmitter(); @HostListener('click') onClick(): void { this._matDialog .open< EditCollectionAttributeComponent, - { collectionAttribute?: Document }, + CollectionAttributeDto, CollectionAttributeDto >(EditCollectionAttributeComponent, { - data: { collectionAttribute: this.collectionAttribute }, + data: this.collectionAttribute, + panelClass: 'bd-bg-image-7', }) .afterClosed() .subscribe((data) => data && this.editCollectionAttribute.emit(data)); diff --git a/libs/bulldozer/client/collections/modals/edit-collection-attribute/src/lib/edit-collection-attribute.module.ts b/libs/bulldozer/client/collections/modals/edit-collection-attribute/src/lib/edit-collection-attribute.module.ts index 8fd93262..a223b185 100644 --- a/libs/bulldozer/client/collections/modals/edit-collection-attribute/src/lib/edit-collection-attribute.module.ts +++ b/libs/bulldozer/client/collections/modals/edit-collection-attribute/src/lib/edit-collection-attribute.module.ts @@ -8,8 +8,8 @@ import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; -import { EditCollectionAttributeTriggerDirective } from './edit-collection-attribute-trigger.directive'; import { EditCollectionAttributeComponent } from './edit-collection-attribute.component'; +import { EditCollectionAttributeDirective } from './edit-collection-attribute.directive'; @NgModule({ imports: [ @@ -25,8 +25,8 @@ import { EditCollectionAttributeComponent } from './edit-collection-attribute.co ], declarations: [ EditCollectionAttributeComponent, - EditCollectionAttributeTriggerDirective, + EditCollectionAttributeDirective, ], - exports: [EditCollectionAttributeTriggerDirective], + exports: [EditCollectionAttributeDirective], }) export class EditCollectionAttributeModule {} diff --git a/libs/bulldozer/client/collections/modals/edit-collection/src/lib/edit-collection.component.ts b/libs/bulldozer/client/collections/modals/edit-collection/src/lib/edit-collection.component.ts index 618a0de4..8604ea05 100644 --- a/libs/bulldozer/client/collections/modals/edit-collection/src/lib/edit-collection.component.ts +++ b/libs/bulldozer/client/collections/modals/edit-collection/src/lib/edit-collection.component.ts @@ -2,13 +2,14 @@ import { Component, HostBinding, Inject } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { Collection, Document } from '@heavy-duty/bulldozer-devkit'; +import { SnackBarComponent } from '@bulldozer-client/notification-snack-bar'; +import { CollectionDto } from '@heavy-duty/bulldozer-devkit'; @Component({ selector: 'bd-edit-collection', template: ` -

- {{ data?.collection ? 'Edit' : 'Create' }} collection +

+ {{ collection ? 'Edit' : 'Create' }} collection

- + + +
+
+
+
+
+
+
- - `, }) export class EditCollectionComponent { @@ -72,12 +74,10 @@ export class EditCollectionComponent { private readonly _matSnackBar: MatSnackBar, private readonly _matDialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) - public data?: { - collection?: Document; - } + public collection?: CollectionDto ) { this.form = new FormGroup({ - name: new FormControl(this.data?.collection?.name ?? '', { + name: new FormControl(this.collection?.name ?? '', { validators: [Validators.required, Validators.maxLength(32)], }), }); @@ -90,9 +90,13 @@ export class EditCollectionComponent { if (this.form.valid) { this._matDialogRef.close(this.form.value); } else { - this._matSnackBar.open('Invalid information', 'close', { - panelClass: 'warning-snackbar', + this._matSnackBar.openFromComponent(SnackBarComponent, { duration: 5000, + data: { + title: 'Heey...', + message: 'Invalid Information', + type: 'warning', + }, }); } } diff --git a/libs/bulldozer/client/collections/modals/edit-collection/src/lib/edit-collection-trigger.directive.ts b/libs/bulldozer/client/collections/modals/edit-collection/src/lib/edit-collection.directive.ts similarity index 51% rename from libs/bulldozer/client/collections/modals/edit-collection/src/lib/edit-collection-trigger.directive.ts rename to libs/bulldozer/client/collections/modals/edit-collection/src/lib/edit-collection.directive.ts index f90aabc6..aa33e2b0 100644 --- a/libs/bulldozer/client/collections/modals/edit-collection/src/lib/edit-collection-trigger.directive.ts +++ b/libs/bulldozer/client/collections/modals/edit-collection/src/lib/edit-collection.directive.ts @@ -6,22 +6,24 @@ import { Output, } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { Collection, Document } from '@heavy-duty/bulldozer-devkit'; +import { CollectionDto } from '@heavy-duty/bulldozer-devkit'; import { EditCollectionComponent } from './edit-collection.component'; -@Directive({ selector: '[bdEditCollectionTrigger]' }) -export class EditCollectionTriggerDirective { - @Input() collection?: Document; - @Output() editCollection = new EventEmitter(); +@Directive({ selector: '[bdEditCollection]' }) +export class EditCollectionDirective { + @Input() collection?: CollectionDto; + @Output() editCollection = new EventEmitter(); @HostListener('click') onClick(): void { this._matDialog - .open< + .open( EditCollectionComponent, - { collection?: Document }, - { name: string } - >(EditCollectionComponent, { data: { collection: this.collection } }) + { + data: this.collection, + panelClass: 'bd-bg-image-7', + } + ) .afterClosed() - .subscribe((data) => data && this.editCollection.emit(data.name)); + .subscribe((data) => data && this.editCollection.emit(data)); } constructor(private readonly _matDialog: MatDialog) {} diff --git a/libs/bulldozer/client/collections/modals/edit-collection/src/lib/edit-collection.module.ts b/libs/bulldozer/client/collections/modals/edit-collection/src/lib/edit-collection.module.ts index 8f4c3d1f..61366526 100644 --- a/libs/bulldozer/client/collections/modals/edit-collection/src/lib/edit-collection.module.ts +++ b/libs/bulldozer/client/collections/modals/edit-collection/src/lib/edit-collection.module.ts @@ -6,8 +6,8 @@ import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; -import { EditCollectionTriggerDirective } from './edit-collection-trigger.directive'; import { EditCollectionComponent } from './edit-collection.component'; +import { EditCollectionDirective } from './edit-collection.directive'; @NgModule({ imports: [ @@ -19,7 +19,7 @@ import { EditCollectionComponent } from './edit-collection.component'; MatIconModule, MatInputModule, ], - declarations: [EditCollectionComponent, EditCollectionTriggerDirective], - exports: [EditCollectionTriggerDirective], + declarations: [EditCollectionComponent, EditCollectionDirective], + exports: [EditCollectionDirective], }) export class EditCollectionModule {} diff --git a/libs/bulldozer/client/collaborators/components/collaborators-list/.eslintrc.json b/libs/bulldozer/client/collections/pages/view-collection-attributes/.eslintrc.json similarity index 100% rename from libs/bulldozer/client/collaborators/components/collaborators-list/.eslintrc.json rename to libs/bulldozer/client/collections/pages/view-collection-attributes/.eslintrc.json diff --git a/libs/bulldozer/client/collections/pages/view-collection-attributes/README.md b/libs/bulldozer/client/collections/pages/view-collection-attributes/README.md new file mode 100644 index 00000000..0dfa26a2 --- /dev/null +++ b/libs/bulldozer/client/collections/pages/view-collection-attributes/README.md @@ -0,0 +1,7 @@ +# bulldozer-client-collections-pages-view-collection-attributes + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test bulldozer-client-collections-pages-view-collection-attributes` to execute the unit tests. diff --git a/libs/bulldozer/client/collections/components/collection-tab/jest.config.js b/libs/bulldozer/client/collections/pages/view-collection-attributes/jest.config.js similarity index 85% rename from libs/bulldozer/client/collections/components/collection-tab/jest.config.js rename to libs/bulldozer/client/collections/pages/view-collection-attributes/jest.config.js index 3bfb6cb3..e4ae6446 100644 --- a/libs/bulldozer/client/collections/components/collection-tab/jest.config.js +++ b/libs/bulldozer/client/collections/pages/view-collection-attributes/jest.config.js @@ -1,5 +1,5 @@ module.exports = { - displayName: 'bulldozer-client-collections-components-collection-tab', + displayName: 'bulldozer-client-collections-pages-view-collection-attributes', preset: '../../../../../../jest.preset.js', setupFilesAfterEnv: ['/src/test-setup.ts'], globals: { @@ -9,7 +9,7 @@ module.exports = { }, }, coverageDirectory: - '../../../../../../coverage/libs/bulldozer/client/collections/components/collection-tab', + '../../../../../../coverage/libs/bulldozer/client/collections/pages/view-collection-attributes', transform: { '^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular', }, diff --git a/libs/bulldozer/client/collections/pages/view-collection-attributes/project.json b/libs/bulldozer/client/collections/pages/view-collection-attributes/project.json new file mode 100644 index 00000000..c69086c1 --- /dev/null +++ b/libs/bulldozer/client/collections/pages/view-collection-attributes/project.json @@ -0,0 +1,28 @@ +{ + "projectType": "library", + "root": "libs/bulldozer/client/collections/pages/view-collection-attributes", + "sourceRoot": "libs/bulldozer/client/collections/pages/view-collection-attributes/src", + "prefix": "bd", + "targets": { + "test": { + "executor": "@nrwl/jest:jest", + "outputs": [ + "coverage/libs/bulldozer/client/collections/pages/view-collection-attributes" + ], + "options": { + "jestConfig": "libs/bulldozer/client/collections/pages/view-collection-attributes/jest.config.js", + "passWithNoTests": true + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "libs/bulldozer/client/collections/pages/view-collection-attributes/src/**/*.ts", + "libs/bulldozer/client/collections/pages/view-collection-attributes/src/**/*.html" + ] + } + } + }, + "tags": [] +} diff --git a/libs/bulldozer/client/collections/pages/view-collection-attributes/src/index.ts b/libs/bulldozer/client/collections/pages/view-collection-attributes/src/index.ts new file mode 100644 index 00000000..5a6c72de --- /dev/null +++ b/libs/bulldozer/client/collections/pages/view-collection-attributes/src/index.ts @@ -0,0 +1 @@ +export * from './lib/view-collection-attributes.module'; diff --git a/libs/bulldozer/client/collections/pages/view-collection-attributes/src/lib/reduce-instructions.ts b/libs/bulldozer/client/collections/pages/view-collection-attributes/src/lib/reduce-instructions.ts new file mode 100644 index 00000000..7d45e54a --- /dev/null +++ b/libs/bulldozer/client/collections/pages/view-collection-attributes/src/lib/reduce-instructions.ts @@ -0,0 +1,293 @@ +import { + CollectionAttributeDto, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { List } from 'immutable'; +import { CollectionAttributeItemView } from './types'; + +const decodeAttributeKind = ( + id: number, + size: number +): { id: number; name: string; size: number } => { + switch (id) { + case 0: + return { + id, + name: 'boolean', + size, + }; + case 1: + return { + id, + name: 'number', + size, + }; + case 2: + return { + id, + name: 'string', + size, + }; + case 3: + return { + id, + name: 'pubkey', + size, + }; + default: + throw Error('Invalid kind id'); + } +}; + +const decodeAttributeModifier = ( + id: number, + size: number +): { id: number; name: string; size: number } => { + switch (id) { + case 0: + return { + id, + name: 'array', + size, + }; + case 1: + return { + id, + name: 'vector', + size, + }; + default: + throw Error('Invalid kind id'); + } +}; + +const getAttributeKindSize = ( + kind: number, + extensions: { max: number | null; maxLength: number | null } +): number => { + switch (kind) { + case 0: + return 1; + case 1: + if (extensions.max === null) { + throw Error('Max is required for numbers'); + } + return extensions.max; + case 2: + if (extensions.maxLength === null) { + throw Error('Max length is required for strings'); + } + return extensions.maxLength; + case 3: + return 32; + default: + throw Error('Invalid Attribute Kind'); + } +}; + +export const reduceInstructions = ( + items: List, + instruction: InstructionStatus +): List => { + switch (instruction.name) { + case 'createCollectionAttribute': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: CollectionAttributeDto; + }; + const name = data.arguments.name; + const kind = decodeAttributeKind( + data.arguments.kind, + getAttributeKindSize(data.arguments.kind, { + max: data.arguments.max, + maxLength: data.arguments.maxLength, + }) + ); + let modifier: { + id: number; + name: string; + size: number; + } | null = null; + if (data.arguments.modifier !== null && data.arguments.size !== null) { + modifier = decodeAttributeModifier( + data.arguments.modifier, + data.arguments.size + ); + } + + const workspaceId = instruction.accounts.find( + (account) => account.name === 'Workspace' + )?.pubkey; + const applicationId = instruction.accounts.find( + (account) => account.name === 'Application' + )?.pubkey; + const collectionId = instruction.accounts.find( + (account) => account.name === 'Collection' + )?.pubkey; + const argumentId = instruction.accounts.find( + (account) => account.name === 'Attribute' + )?.pubkey; + + if ( + workspaceId === undefined || + applicationId === undefined || + collectionId === undefined || + argumentId === undefined + ) { + throw new Error('Malformed Create Collection Attribute'); + } + + const itemIndex = items.findIndex((item) => item.id === argumentId); + + if (itemIndex === -1) { + return items.push({ + id: argumentId, + name, + kind, + modifier, + isCreating: true, + isUpdating: false, + isDeleting: false, + collectionId, + applicationId, + workspaceId, + }); + } else { + return items.update(itemIndex, (item) => ({ + ...item, + id: argumentId, + name, + kind, + modifier, + isCreating: true, + isUpdating: false, + isDeleting: false, + collectionId, + applicationId, + workspaceId, + })); + } + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Attribute' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isCreating: false, + }; + }); + } else { + return items; + } + } + case 'updateCollectionAttribute': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: CollectionAttributeDto; + }; + const name = data.arguments.name; + const kind = decodeAttributeKind( + data.arguments.kind, + getAttributeKindSize(data.arguments.kind, { + max: data.arguments.max, + maxLength: data.arguments.maxLength, + }) + ); + let modifier: { + id: number; + name: string; + size: number; + } | null = null; + if (data.arguments.modifier !== null && data.arguments.size !== null) { + modifier = decodeAttributeModifier( + data.arguments.modifier, + data.arguments.size + ); + } + + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Attribute' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + name, + kind, + modifier, + isUpdating: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Attribute' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isUpdating: false, + }; + }); + } else { + return items; + } + } + case 'deleteCollectionAttribute': + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Attribute' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isDeleting: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.filter( + (item) => + !instruction.accounts.some( + (account) => + account.name === 'Attribute' && account.pubkey === item.id + ) + ); + } else { + return items; + } + default: + return items; + } +}; diff --git a/libs/bulldozer/client/collections/pages/view-collection-attributes/src/lib/types.ts b/libs/bulldozer/client/collections/pages/view-collection-attributes/src/lib/types.ts new file mode 100644 index 00000000..bbb665c1 --- /dev/null +++ b/libs/bulldozer/client/collections/pages/view-collection-attributes/src/lib/types.ts @@ -0,0 +1,20 @@ +export interface CollectionAttributeItemView { + id: string; + name: string; + kind: { + id: number; + name: string; + size: number; + }; + modifier: { + id: number; + name: string; + size: number; + } | null; + isCreating: boolean; + isUpdating: boolean; + isDeleting: boolean; + collectionId: string; + applicationId: string; + workspaceId: string; +} diff --git a/libs/bulldozer/client/collections/pages/view-collection-attributes/src/lib/view-collection-attributes.component.ts b/libs/bulldozer/client/collections/pages/view-collection-attributes/src/lib/view-collection-attributes.component.ts new file mode 100644 index 00000000..85df2370 --- /dev/null +++ b/libs/bulldozer/client/collections/pages/view-collection-attributes/src/lib/view-collection-attributes.component.ts @@ -0,0 +1,379 @@ +import { Component, HostBinding, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { + CollectionAttributeApiService, + CollectionAttributeQueryStore, + CollectionAttributesStore, + CollectionStore, +} from '@bulldozer-client/collections-data-access'; +import { NotificationStore } from '@bulldozer-client/notifications-data-access'; +import { HdBroadcasterSocketStore } from '@heavy-duty/broadcaster'; +import { CollectionAttributeDto } from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { Keypair } from '@solana/web3.js'; +import { distinctUntilChanged, map } from 'rxjs'; +import { ViewCollectionAttributesStore } from './view-collection-attributes.store'; + +@Component({ + selector: 'bd-view-collection-attributes', + template: ` +
+
+

Attributes

+

+ The attributes of all the elements that compose a collection +

+
+ + + + + + + + + + +
+ +
+
+
+ +
+ +
+ +
+ + +

+ + Creating + + + Updating + + + Deleting + +

+
+ +
+
+

+ {{ collectionAttribute.name }} +

+ +

+ {{ collectionAttribute.kind.name }}. +

+
+
+

+ auto_awesome_motion +   + + + Array of Items + + + Vector ofItems + + + + + Single Item + +

+

+ data_array +   Size: + {{ collectionAttribute.modifier?.size }} +

+
+
+
+ + + + + + +
+
+ + +

There's no attributes yet.

+
+
+ `, + styles: [], + providers: [ + CollectionStore, + CollectionAttributesStore, + CollectionAttributeQueryStore, + ViewCollectionAttributesStore, + ], +}) +export class ViewCollectionAttributesComponent implements OnInit { + @HostBinding('class') class = 'block p-8 pt-5 h-full'; + + readonly collectionAttributes$ = + this._viewCollectionAttributesStore.collectionAttributes$; + + readonly workspaceId$ = this._route.paramMap.pipe( + map((paramMap) => paramMap.get('workspaceId')), + isNotNullOrUndefined, + distinctUntilChanged() + ); + readonly applicationId$ = this._route.paramMap.pipe( + map((paramMap) => paramMap.get('applicationId')), + isNotNullOrUndefined, + distinctUntilChanged() + ); + readonly collectionId$ = this._route.paramMap.pipe( + map((paramMap) => paramMap.get('collectionId')), + isNotNullOrUndefined, + distinctUntilChanged() + ); + + constructor( + private readonly _route: ActivatedRoute, + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _notificationStore: NotificationStore, + private readonly _collectionAttributeApiService: CollectionAttributeApiService, + private readonly _viewCollectionAttributesStore: ViewCollectionAttributesStore + ) {} + + ngOnInit() { + this._viewCollectionAttributesStore.setCollectionId(this.collectionId$); + } + + onCreateCollectionAttribute( + authority: string, + workspaceId: string, + applicationId: string, + collectionId: string, + collectionAttributeDto: CollectionAttributeDto + ) { + const collectionAttributeKeypair = Keypair.generate(); + + this._collectionAttributeApiService + .create(collectionAttributeKeypair, { + collectionAttributeDto, + authority, + workspaceId, + applicationId, + collectionId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Create attribute request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `collections:${collectionId}:attributes`, + `collectionAttributes:${collectionAttributeKeypair.publicKey.toBase58()}`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); + } + + onUpdateCollectionAttribute( + authority: string, + workspaceId: string, + collectionId: string, + collectionAttributeId: string, + collectionAttributeDto: CollectionAttributeDto + ) { + this._collectionAttributeApiService + .update({ + authority, + workspaceId, + collectionId, + collectionAttributeDto, + collectionAttributeId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Update attribute request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `collections:${collectionId}:attributes`, + `collectionAttributes:${collectionAttributeId}`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); + } + + onDeleteCollectionAttribute( + authority: string, + workspaceId: string, + collectionId: string, + collectionAttributeId: string + ) { + this._collectionAttributeApiService + .delete({ + authority, + workspaceId, + collectionAttributeId, + collectionId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Delete attribute request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `collections:${collectionId}:attributes`, + `collectionAttributes:${collectionAttributeId}`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); + } +} diff --git a/libs/bulldozer/client/collections/components/collection-attributes-list/src/lib/collection-attributes-list.module.ts b/libs/bulldozer/client/collections/pages/view-collection-attributes/src/lib/view-collection-attributes.module.ts similarity index 64% rename from libs/bulldozer/client/collections/components/collection-attributes-list/src/lib/collection-attributes-list.module.ts rename to libs/bulldozer/client/collections/pages/view-collection-attributes/src/lib/view-collection-attributes.module.ts index 0812f276..95d2cd59 100644 --- a/libs/bulldozer/client/collections/components/collection-attributes-list/src/lib/collection-attributes-list.module.ts +++ b/libs/bulldozer/client/collections/pages/view-collection-attributes/src/lib/view-collection-attributes.module.ts @@ -7,16 +7,24 @@ import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { RouterModule } from '@angular/router'; +import { CardModule } from '@bulldozer-client/bd-card'; import { EditCollectionAttributeModule } from '@bulldozer-client/edit-collection-attribute'; import { ItemUpdatingModule } from '@bulldozer-client/item-updating'; -import { SectionHeaderModule } from '@bulldozer-client/section-header'; +import { HdWalletAdapterCdkModule } from '@heavy-duty/wallet-adapter-cdk'; import { ReactiveComponentModule } from '@ngrx/component'; -import { CollectionAttributesListComponent } from './collection-attributes-list.component'; +import { ViewCollectionAttributesComponent } from './view-collection-attributes.component'; @NgModule({ - declarations: [CollectionAttributesListComponent], imports: [ CommonModule, + RouterModule.forChild([ + { + path: '', + pathMatch: 'full', + component: ViewCollectionAttributesComponent, + }, + ]), MatButtonModule, MatCardModule, MatIconModule, @@ -25,10 +33,11 @@ import { CollectionAttributesListComponent } from './collection-attributes-list. MatProgressSpinnerModule, MatTooltipModule, ReactiveComponentModule, - SectionHeaderModule, - EditCollectionAttributeModule, + HdWalletAdapterCdkModule, ItemUpdatingModule, + EditCollectionAttributeModule, + CardModule, ], - exports: [CollectionAttributesListComponent], + declarations: [ViewCollectionAttributesComponent], }) -export class CollectionAttributesListModule {} +export class ViewCollectionAttributesModule {} diff --git a/libs/bulldozer/client/collections/pages/view-collection-attributes/src/lib/view-collection-attributes.store.ts b/libs/bulldozer/client/collections/pages/view-collection-attributes/src/lib/view-collection-attributes.store.ts new file mode 100644 index 00000000..d8dc072f --- /dev/null +++ b/libs/bulldozer/client/collections/pages/view-collection-attributes/src/lib/view-collection-attributes.store.ts @@ -0,0 +1,201 @@ +import { Injectable } from '@angular/core'; +import { CollectionAttributesStore } from '@bulldozer-client/collections-data-access'; +import { + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; +import { + CollectionAttribute, + Document, + flattenInstructions, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined, isTruthy } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-instructions'; +import { CollectionAttributeItemView } from './types'; + +const documentToView = ( + document: Document +): CollectionAttributeItemView => { + return { + id: document.id, + name: document.name, + isCreating: false, + isUpdating: false, + isDeleting: false, + kind: document.data.kind, + modifier: document.data.modifier, + collectionId: document.data.collection, + applicationId: document.data.application, + workspaceId: document.data.workspace, + }; +}; + +interface ViewModel { + collectionId: string | null; + transactions: List; +} + +const initialState: ViewModel = { + collectionId: null, + transactions: List(), +}; + +@Injectable() +export class ViewCollectionAttributesStore extends ComponentStore { + private readonly _collectionId$ = this.select( + ({ collectionId }) => collectionId + ); + private readonly _topicName$ = this.select( + this._collectionId$.pipe(isNotNullOrUndefined), + (collectionId) => `collections:${collectionId}:attributes` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly collectionAttributes$ = this.select( + this._collectionAttributesStore.collectionAttributes$, + this._instructionStatuses$, + (collectionAttributes, instructionStatuses) => { + if (collectionAttributes === null) { + return null; + } + + return instructionStatuses.reduce( + reduceInstructions, + collectionAttributes.map(documentToView) + ); + }, + { debounce: true } + ); + + constructor( + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _collectionAttributesStore: CollectionAttributesStore + ) { + super(initialState); + + this._collectionAttributesStore.setFilters( + this.select( + this._collectionId$.pipe(isNotNullOrUndefined), + this._hdBroadcasterSocketStore.connected$.pipe(isTruthy), + (collectionId) => ({ collection: collectionId }) + ) + ); + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) + ) + ); + } + + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) + ); + + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) + ); + + readonly setCollectionId = this.updater( + (state, collectionId) => ({ + ...state, + collectionId, + }) + ); + + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); + } + }) + ); + + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; + }>( + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } + + this.patchState({ transactions: List() }); + + const correlationId = uuid(); + let subscriptionId: string; + + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } + + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName + ); + } + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } + }) + ); + }) + ); +} diff --git a/libs/bulldozer/client/collections/components/collection-attributes-list/src/test-setup.ts b/libs/bulldozer/client/collections/pages/view-collection-attributes/src/test-setup.ts similarity index 100% rename from libs/bulldozer/client/collections/components/collection-attributes-list/src/test-setup.ts rename to libs/bulldozer/client/collections/pages/view-collection-attributes/src/test-setup.ts diff --git a/libs/bulldozer/client/collaborators/components/collaborators-list/tsconfig.json b/libs/bulldozer/client/collections/pages/view-collection-attributes/tsconfig.json similarity index 100% rename from libs/bulldozer/client/collaborators/components/collaborators-list/tsconfig.json rename to libs/bulldozer/client/collections/pages/view-collection-attributes/tsconfig.json diff --git a/libs/bulldozer/client/collaborators/components/collaborators-list/tsconfig.lib.json b/libs/bulldozer/client/collections/pages/view-collection-attributes/tsconfig.lib.json similarity index 100% rename from libs/bulldozer/client/collaborators/components/collaborators-list/tsconfig.lib.json rename to libs/bulldozer/client/collections/pages/view-collection-attributes/tsconfig.lib.json diff --git a/libs/bulldozer/client/collaborators/components/collaborators-list/tsconfig.spec.json b/libs/bulldozer/client/collections/pages/view-collection-attributes/tsconfig.spec.json similarity index 100% rename from libs/bulldozer/client/collaborators/components/collaborators-list/tsconfig.spec.json rename to libs/bulldozer/client/collections/pages/view-collection-attributes/tsconfig.spec.json diff --git a/libs/bulldozer/client/collections/components/collection-attributes-list/.eslintrc.json b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/.eslintrc.json similarity index 100% rename from libs/bulldozer/client/collections/components/collection-attributes-list/.eslintrc.json rename to libs/bulldozer/client/collections/pages/view-collection-code-viewer/.eslintrc.json diff --git a/libs/bulldozer/client/collections/pages/view-collection-code-viewer/README.md b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/README.md new file mode 100644 index 00000000..7ae35d18 --- /dev/null +++ b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/README.md @@ -0,0 +1,7 @@ +# bulldozer-client-collections-pages-view-collection-code-viewer + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test bulldozer-client-collections-pages-view-collection-code-viewer` to execute the unit tests. diff --git a/libs/bulldozer/client/collections/components/collection-attributes-list/jest.config.js b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/jest.config.js similarity index 84% rename from libs/bulldozer/client/collections/components/collection-attributes-list/jest.config.js rename to libs/bulldozer/client/collections/pages/view-collection-code-viewer/jest.config.js index c37ae8af..03d3a220 100644 --- a/libs/bulldozer/client/collections/components/collection-attributes-list/jest.config.js +++ b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/jest.config.js @@ -1,6 +1,5 @@ module.exports = { - displayName: - 'bulldozer-client-collections-components-collection-attributes-list', + displayName: 'bulldozer-client-collections-pages-view-collection-code-viewer', preset: '../../../../../../jest.preset.js', setupFilesAfterEnv: ['/src/test-setup.ts'], globals: { @@ -10,7 +9,7 @@ module.exports = { }, }, coverageDirectory: - '../../../../../../coverage/libs/bulldozer/client/collections/components/collection-attributes-list', + '../../../../../../coverage/libs/bulldozer/client/collections/pages/view-collection-code-viewer', transform: { '^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular', }, diff --git a/libs/bulldozer/client/collections/pages/view-collection-code-viewer/project.json b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/project.json new file mode 100644 index 00000000..86802904 --- /dev/null +++ b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/project.json @@ -0,0 +1,28 @@ +{ + "projectType": "library", + "root": "libs/bulldozer/client/collections/pages/view-collection-code-viewer", + "sourceRoot": "libs/bulldozer/client/collections/pages/view-collection-code-viewer/src", + "prefix": "bd", + "targets": { + "test": { + "executor": "@nrwl/jest:jest", + "outputs": [ + "coverage/libs/bulldozer/client/collections/pages/view-collection-code-viewer" + ], + "options": { + "jestConfig": "libs/bulldozer/client/collections/pages/view-collection-code-viewer/jest.config.js", + "passWithNoTests": true + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/**/*.ts", + "libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/**/*.html" + ] + } + } + }, + "tags": [] +} diff --git a/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/index.ts b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/index.ts new file mode 100644 index 00000000..c38f4d9f --- /dev/null +++ b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/index.ts @@ -0,0 +1 @@ +export * from './lib/view-collection-code.module'; diff --git a/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/reduce-attributes-instructions.ts b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/reduce-attributes-instructions.ts new file mode 100644 index 00000000..7d45e54a --- /dev/null +++ b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/reduce-attributes-instructions.ts @@ -0,0 +1,293 @@ +import { + CollectionAttributeDto, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { List } from 'immutable'; +import { CollectionAttributeItemView } from './types'; + +const decodeAttributeKind = ( + id: number, + size: number +): { id: number; name: string; size: number } => { + switch (id) { + case 0: + return { + id, + name: 'boolean', + size, + }; + case 1: + return { + id, + name: 'number', + size, + }; + case 2: + return { + id, + name: 'string', + size, + }; + case 3: + return { + id, + name: 'pubkey', + size, + }; + default: + throw Error('Invalid kind id'); + } +}; + +const decodeAttributeModifier = ( + id: number, + size: number +): { id: number; name: string; size: number } => { + switch (id) { + case 0: + return { + id, + name: 'array', + size, + }; + case 1: + return { + id, + name: 'vector', + size, + }; + default: + throw Error('Invalid kind id'); + } +}; + +const getAttributeKindSize = ( + kind: number, + extensions: { max: number | null; maxLength: number | null } +): number => { + switch (kind) { + case 0: + return 1; + case 1: + if (extensions.max === null) { + throw Error('Max is required for numbers'); + } + return extensions.max; + case 2: + if (extensions.maxLength === null) { + throw Error('Max length is required for strings'); + } + return extensions.maxLength; + case 3: + return 32; + default: + throw Error('Invalid Attribute Kind'); + } +}; + +export const reduceInstructions = ( + items: List, + instruction: InstructionStatus +): List => { + switch (instruction.name) { + case 'createCollectionAttribute': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: CollectionAttributeDto; + }; + const name = data.arguments.name; + const kind = decodeAttributeKind( + data.arguments.kind, + getAttributeKindSize(data.arguments.kind, { + max: data.arguments.max, + maxLength: data.arguments.maxLength, + }) + ); + let modifier: { + id: number; + name: string; + size: number; + } | null = null; + if (data.arguments.modifier !== null && data.arguments.size !== null) { + modifier = decodeAttributeModifier( + data.arguments.modifier, + data.arguments.size + ); + } + + const workspaceId = instruction.accounts.find( + (account) => account.name === 'Workspace' + )?.pubkey; + const applicationId = instruction.accounts.find( + (account) => account.name === 'Application' + )?.pubkey; + const collectionId = instruction.accounts.find( + (account) => account.name === 'Collection' + )?.pubkey; + const argumentId = instruction.accounts.find( + (account) => account.name === 'Attribute' + )?.pubkey; + + if ( + workspaceId === undefined || + applicationId === undefined || + collectionId === undefined || + argumentId === undefined + ) { + throw new Error('Malformed Create Collection Attribute'); + } + + const itemIndex = items.findIndex((item) => item.id === argumentId); + + if (itemIndex === -1) { + return items.push({ + id: argumentId, + name, + kind, + modifier, + isCreating: true, + isUpdating: false, + isDeleting: false, + collectionId, + applicationId, + workspaceId, + }); + } else { + return items.update(itemIndex, (item) => ({ + ...item, + id: argumentId, + name, + kind, + modifier, + isCreating: true, + isUpdating: false, + isDeleting: false, + collectionId, + applicationId, + workspaceId, + })); + } + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Attribute' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isCreating: false, + }; + }); + } else { + return items; + } + } + case 'updateCollectionAttribute': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: CollectionAttributeDto; + }; + const name = data.arguments.name; + const kind = decodeAttributeKind( + data.arguments.kind, + getAttributeKindSize(data.arguments.kind, { + max: data.arguments.max, + maxLength: data.arguments.maxLength, + }) + ); + let modifier: { + id: number; + name: string; + size: number; + } | null = null; + if (data.arguments.modifier !== null && data.arguments.size !== null) { + modifier = decodeAttributeModifier( + data.arguments.modifier, + data.arguments.size + ); + } + + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Attribute' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + name, + kind, + modifier, + isUpdating: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Attribute' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isUpdating: false, + }; + }); + } else { + return items; + } + } + case 'deleteCollectionAttribute': + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Attribute' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isDeleting: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.filter( + (item) => + !instruction.accounts.some( + (account) => + account.name === 'Attribute' && account.pubkey === item.id + ) + ); + } else { + return items; + } + default: + return items; + } +}; diff --git a/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/reduce-collection-instructions.ts b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/reduce-collection-instructions.ts new file mode 100644 index 00000000..a18f221b --- /dev/null +++ b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/reduce-collection-instructions.ts @@ -0,0 +1,104 @@ +import { CollectionDto, InstructionStatus } from '@heavy-duty/bulldozer-devkit'; +import { CollectionItemView } from './types'; + +export const reduceInstructions = ( + item: CollectionItemView | null, + instruction: InstructionStatus +): CollectionItemView | null => { + switch (instruction.name) { + case 'createCollection': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: CollectionDto; + }; + const name = data.arguments.name; + + const workspaceId = instruction.accounts.find( + (account) => account.name === 'Workspace' + )?.pubkey; + const applicationId = instruction.accounts.find( + (account) => account.name === 'Application' + )?.pubkey; + const instructionId = instruction.accounts.find( + (account) => account.name === 'Collection' + )?.pubkey; + + if ( + workspaceId === undefined || + applicationId === undefined || + instructionId === undefined + ) { + throw new Error('Malformed Create Collection'); + } + + return { + id: instructionId, + name, + isCreating: true, + isUpdating: false, + isDeleting: false, + applicationId, + workspaceId, + }; + } else if (instruction.transactionStatus.status === 'finalized') { + if (item === null) { + return null; + } else { + return { + ...item, + isCreating: false, + }; + } + } else { + return item; + } + } + case 'updateCollection': { + if (item === null) { + return item; + } else if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: CollectionDto; + }; + const name = data.arguments.name; + + return { + ...item, + name, + isUpdating: true, + }; + } else if (instruction.transactionStatus.status === 'finalized') { + return { + ...item, + isUpdating: false, + }; + } else { + return item; + } + } + case 'deleteCollection': + if (item === null) { + return item; + } else if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + return { + ...item, + isDeleting: true, + }; + } else if (instruction.transactionStatus.status === 'finalized') { + return null; + } else { + return item; + } + default: + return item; + } +}; diff --git a/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/types.ts b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/types.ts new file mode 100644 index 00000000..f9e11e6c --- /dev/null +++ b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/types.ts @@ -0,0 +1,30 @@ +export interface CollectionItemView { + id: string; + name: string; + isCreating: boolean; + isUpdating: boolean; + isDeleting: boolean; + applicationId: string; + workspaceId: string; +} + +export interface CollectionAttributeItemView { + id: string; + name: string; + kind: { + id: number; + name: string; + size: number; + }; + modifier: { + id: number; + name: string; + size: number; + } | null; + isCreating: boolean; + isUpdating: boolean; + isDeleting: boolean; + collectionId: string; + applicationId: string; + workspaceId: string; +} diff --git a/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/view-collection-code-attributes.store.ts b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/view-collection-code-attributes.store.ts new file mode 100644 index 00000000..7de0f2ff --- /dev/null +++ b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/view-collection-code-attributes.store.ts @@ -0,0 +1,201 @@ +import { Injectable } from '@angular/core'; +import { CollectionAttributesStore } from '@bulldozer-client/collections-data-access'; +import { + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; +import { + CollectionAttribute, + Document, + flattenInstructions, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined, isTruthy } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-attributes-instructions'; +import { CollectionAttributeItemView } from './types'; + +const documentToView = ( + document: Document +): CollectionAttributeItemView => { + return { + id: document.id, + name: document.name, + isCreating: false, + isUpdating: false, + isDeleting: false, + kind: document.data.kind, + modifier: document.data.modifier, + collectionId: document.data.collection, + applicationId: document.data.application, + workspaceId: document.data.workspace, + }; +}; + +interface ViewModel { + collectionId: string | null; + transactions: List; +} + +const initialState: ViewModel = { + collectionId: null, + transactions: List(), +}; + +@Injectable() +export class ViewCollectionCodeAttributesStore extends ComponentStore { + private readonly _collectionId$ = this.select( + ({ collectionId }) => collectionId + ); + private readonly _topicName$ = this.select( + this._collectionId$.pipe(isNotNullOrUndefined), + (collectionId) => `collections:${collectionId}:attributes` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly collectionAttributes$ = this.select( + this._collectionAttributesStore.collectionAttributes$, + this._instructionStatuses$, + (collectionAttributes, instructionStatuses) => { + if (collectionAttributes === null) { + return null; + } + + return instructionStatuses.reduce( + reduceInstructions, + collectionAttributes.map(documentToView) + ); + }, + { debounce: true } + ); + + constructor( + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _collectionAttributesStore: CollectionAttributesStore + ) { + super(initialState); + + this._collectionAttributesStore.setFilters( + this.select( + this._collectionId$.pipe(isNotNullOrUndefined), + this._hdBroadcasterSocketStore.connected$.pipe(isTruthy), + (collectionId) => ({ collection: collectionId }) + ) + ); + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) + ) + ); + } + + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) + ); + + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) + ); + + readonly setCollectionId = this.updater( + (state, collectionId) => ({ + ...state, + collectionId, + }) + ); + + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); + } + }) + ); + + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; + }>( + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } + + this.patchState({ transactions: List() }); + + const correlationId = uuid(); + let subscriptionId: string; + + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } + + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName + ); + } + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } + }) + ); + }) + ); +} diff --git a/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/view-collection-code-collection.store.ts b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/view-collection-code-collection.store.ts new file mode 100644 index 00000000..ef12a908 --- /dev/null +++ b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/view-collection-code-collection.store.ts @@ -0,0 +1,189 @@ +import { Injectable } from '@angular/core'; +import { CollectionStore } from '@bulldozer-client/collections-data-access'; +import { + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; +import { + Collection, + Document, + flattenInstructions, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined, isTruthy } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-collection-instructions'; +import { CollectionItemView } from './types'; + +const documentToView = (document: Document): CollectionItemView => { + return { + id: document.id, + name: document.name, + isCreating: false, + isUpdating: false, + isDeleting: false, + applicationId: document.data.application, + workspaceId: document.data.workspace, + }; +}; + +interface ViewModel { + collectionId: string | null; + transactions: List; +} + +const initialState: ViewModel = { + collectionId: null, + transactions: List(), +}; + +@Injectable() +export class ViewCollectionCodeCollectionStore extends ComponentStore { + readonly collectionId$ = this.select(({ collectionId }) => collectionId); + private readonly _topicName$ = this.select( + this.collectionId$.pipe(isNotNullOrUndefined), + (collectionId) => `collections:${collectionId}` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly collection$ = this.select( + this._collectionStore.collection$, + this._instructionStatuses$, + (collection, instructionStatuses) => + instructionStatuses.reduce( + reduceInstructions, + collection === null ? null : documentToView(collection) + ), + { debounce: true } + ); + + constructor( + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _collectionStore: CollectionStore + ) { + super(initialState); + + this._collectionStore.setCollectionId( + this.select( + this.collectionId$.pipe(isNotNullOrUndefined), + this._hdBroadcasterSocketStore.connected$.pipe(isTruthy), + (collectionId) => collectionId + ) + ); + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) + ) + ); + } + + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) + ); + + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) + ); + + readonly setCollectionId = this.updater( + (state, collectionId) => ({ + ...state, + collectionId, + }) + ); + + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); + } + }) + ); + + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; + }>( + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } + + this.patchState({ transactions: List() }); + + const correlationId = uuid(); + let subscriptionId: string; + + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } + + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName + ); + } + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } + }) + ); + }) + ); +} diff --git a/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/view-collection-code.component.ts b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/view-collection-code.component.ts new file mode 100644 index 00000000..9cec1f12 --- /dev/null +++ b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/view-collection-code.component.ts @@ -0,0 +1,93 @@ +import { Component, HostBinding, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { + CollectionAttributesStore, + CollectionStore, +} from '@bulldozer-client/collections-data-access'; +import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { distinctUntilChanged, map } from 'rxjs'; +import { ViewCollectionCodeAttributesStore } from './view-collection-code-attributes.store'; +import { ViewCollectionCodeCollectionStore } from './view-collection-code-collection.store'; +import { ViewCollectionCodeStore } from './view-collection-code.store'; + +@Component({ + selector: 'bd-view-collection-code', + template: ` +
+

Code Viewer

+

+ The code editor allows you to customize a collection. +

+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `, + styles: [], + providers: [ + CollectionStore, + CollectionAttributesStore, + ViewCollectionCodeStore, + ViewCollectionCodeCollectionStore, + ViewCollectionCodeAttributesStore, + ], +}) +export class ViewCollectionCodeComponent implements OnInit { + @HostBinding('class') class = 'flex flex-col p-8 pt-5 h-full'; + + readonly code$ = this._viewCollectionCodeStore.code$; + readonly collectionId$ = this._route.paramMap.pipe( + map((paramMap) => paramMap.get('collectionId')), + isNotNullOrUndefined, + distinctUntilChanged() + ); + + constructor( + private readonly _route: ActivatedRoute, + private readonly _viewCollectionCodeStore: ViewCollectionCodeStore, + private readonly _viewCollectionCodeAttributesStore: ViewCollectionCodeAttributesStore, + private readonly _viewCollectionCodeCollectionStore: ViewCollectionCodeCollectionStore + ) {} + + ngOnInit() { + this._viewCollectionCodeAttributesStore.setCollectionId(this.collectionId$); + this._viewCollectionCodeCollectionStore.setCollectionId(this.collectionId$); + } +} diff --git a/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/view-collection-code.module.ts b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/view-collection-code.module.ts new file mode 100644 index 00000000..42c9cbf0 --- /dev/null +++ b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/view-collection-code.module.ts @@ -0,0 +1,23 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { CodeEditorModule } from '@bulldozer-client/code-editor'; +import { ReactiveComponentModule } from '@ngrx/component'; +import { ViewCollectionCodeComponent } from './view-collection-code.component'; + +@NgModule({ + imports: [ + CommonModule, + RouterModule.forChild([ + { + path: '', + pathMatch: 'full', + component: ViewCollectionCodeComponent, + }, + ]), + ReactiveComponentModule, + CodeEditorModule, + ], + declarations: [ViewCollectionCodeComponent], +}) +export class ViewCollectionCodeModule {} diff --git a/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/view-collection-code.store.ts b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/view-collection-code.store.ts new file mode 100644 index 00000000..249e10e0 --- /dev/null +++ b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/lib/view-collection-code.store.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@angular/core'; +import { generateCollectionCode2 } from '@heavy-duty/generator'; +import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { List } from 'immutable'; +import { CollectionAttributeItemView, CollectionItemView } from './types'; +import { ViewCollectionCodeAttributesStore } from './view-collection-code-attributes.store'; +import { ViewCollectionCodeCollectionStore } from './view-collection-code-collection.store'; + +interface ViewModel { + code: string | null; +} + +const initialState: ViewModel = { + code: null, +}; + +@Injectable() +export class ViewCollectionCodeStore extends ComponentStore { + readonly code$ = this.select(({ code }) => code); + + constructor( + private readonly _viewCollectionCodeCollectionStore: ViewCollectionCodeCollectionStore, + private readonly _viewCollectionCodeAttributesStore: ViewCollectionCodeAttributesStore + ) { + super(initialState); + + this._loadCode( + this.select( + this._viewCollectionCodeCollectionStore.collection$.pipe( + isNotNullOrUndefined + ), + this._viewCollectionCodeAttributesStore.collectionAttributes$.pipe( + isNotNullOrUndefined + ), + (collection, collectionAttributes) => ({ + collection: collection ?? null, + collectionAttributes: collectionAttributes, + }), + { debounce: true } + ) + ); + } + + private readonly _loadCode = this.updater<{ + collection: CollectionItemView; + collectionAttributes: List; + }>((state, { collection, collectionAttributes }) => ({ + ...state, + code: generateCollectionCode2(collection, collectionAttributes), + })); +} diff --git a/libs/bulldozer/client/collections/components/collection-tab/src/test-setup.ts b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/test-setup.ts similarity index 100% rename from libs/bulldozer/client/collections/components/collection-tab/src/test-setup.ts rename to libs/bulldozer/client/collections/pages/view-collection-code-viewer/src/test-setup.ts diff --git a/libs/bulldozer/client/collections/components/collection-attributes-list/tsconfig.json b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/tsconfig.json similarity index 100% rename from libs/bulldozer/client/collections/components/collection-attributes-list/tsconfig.json rename to libs/bulldozer/client/collections/pages/view-collection-code-viewer/tsconfig.json diff --git a/libs/bulldozer/client/collections/components/collection-attributes-list/tsconfig.lib.json b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/tsconfig.lib.json similarity index 100% rename from libs/bulldozer/client/collections/components/collection-attributes-list/tsconfig.lib.json rename to libs/bulldozer/client/collections/pages/view-collection-code-viewer/tsconfig.lib.json diff --git a/libs/bulldozer/client/collections/components/collection-attributes-list/tsconfig.spec.json b/libs/bulldozer/client/collections/pages/view-collection-code-viewer/tsconfig.spec.json similarity index 100% rename from libs/bulldozer/client/collections/components/collection-attributes-list/tsconfig.spec.json rename to libs/bulldozer/client/collections/pages/view-collection-code-viewer/tsconfig.spec.json diff --git a/libs/bulldozer/client/collections/pages/view-collection/src/index.ts b/libs/bulldozer/client/collections/pages/view-collection/src/index.ts index eada614a..1ecb652a 100644 --- a/libs/bulldozer/client/collections/pages/view-collection/src/index.ts +++ b/libs/bulldozer/client/collections/pages/view-collection/src/index.ts @@ -1 +1,2 @@ +export * from './lib/view-collection-tab.module'; export * from './lib/view-collection.module'; diff --git a/libs/bulldozer/client/collections/pages/view-collection/src/lib/reduce-instructions.ts b/libs/bulldozer/client/collections/pages/view-collection/src/lib/reduce-instructions.ts new file mode 100644 index 00000000..a18f221b --- /dev/null +++ b/libs/bulldozer/client/collections/pages/view-collection/src/lib/reduce-instructions.ts @@ -0,0 +1,104 @@ +import { CollectionDto, InstructionStatus } from '@heavy-duty/bulldozer-devkit'; +import { CollectionItemView } from './types'; + +export const reduceInstructions = ( + item: CollectionItemView | null, + instruction: InstructionStatus +): CollectionItemView | null => { + switch (instruction.name) { + case 'createCollection': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: CollectionDto; + }; + const name = data.arguments.name; + + const workspaceId = instruction.accounts.find( + (account) => account.name === 'Workspace' + )?.pubkey; + const applicationId = instruction.accounts.find( + (account) => account.name === 'Application' + )?.pubkey; + const instructionId = instruction.accounts.find( + (account) => account.name === 'Collection' + )?.pubkey; + + if ( + workspaceId === undefined || + applicationId === undefined || + instructionId === undefined + ) { + throw new Error('Malformed Create Collection'); + } + + return { + id: instructionId, + name, + isCreating: true, + isUpdating: false, + isDeleting: false, + applicationId, + workspaceId, + }; + } else if (instruction.transactionStatus.status === 'finalized') { + if (item === null) { + return null; + } else { + return { + ...item, + isCreating: false, + }; + } + } else { + return item; + } + } + case 'updateCollection': { + if (item === null) { + return item; + } else if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: CollectionDto; + }; + const name = data.arguments.name; + + return { + ...item, + name, + isUpdating: true, + }; + } else if (instruction.transactionStatus.status === 'finalized') { + return { + ...item, + isUpdating: false, + }; + } else { + return item; + } + } + case 'deleteCollection': + if (item === null) { + return item; + } else if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + return { + ...item, + isDeleting: true, + }; + } else if (instruction.transactionStatus.status === 'finalized') { + return null; + } else { + return item; + } + default: + return item; + } +}; diff --git a/libs/bulldozer/client/collections/pages/view-collection/src/lib/types.ts b/libs/bulldozer/client/collections/pages/view-collection/src/lib/types.ts new file mode 100644 index 00000000..64ef18a8 --- /dev/null +++ b/libs/bulldozer/client/collections/pages/view-collection/src/lib/types.ts @@ -0,0 +1,9 @@ +export interface CollectionItemView { + id: string; + name: string; + isCreating: boolean; + isUpdating: boolean; + isDeleting: boolean; + applicationId: string; + workspaceId: string; +} diff --git a/libs/bulldozer/client/collections/pages/view-collection/src/lib/view-collection-code.store.ts b/libs/bulldozer/client/collections/pages/view-collection/src/lib/view-collection-code.store.ts deleted file mode 100644 index c181bb64..00000000 --- a/libs/bulldozer/client/collections/pages/view-collection/src/lib/view-collection-code.store.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Injectable } from '@angular/core'; -import { CodeEditorOptions } from '@bulldozer-client/code-editor'; -import { - CollectionAttributesStore, - CollectionStore, -} from '@bulldozer-client/collections-data-access'; -import { DarkThemeStore } from '@bulldozer-client/core-data-access'; -import { - Collection, - CollectionAttribute, - Document, -} from '@heavy-duty/bulldozer-devkit'; -import { generateCollectionCode } from '@heavy-duty/generator'; -import { ComponentStore } from '@ngrx/component-store'; - -interface ViewModel { - code: string | null; - editorOptions: CodeEditorOptions | null; -} - -const initialState: ViewModel = { - code: null, - editorOptions: null, -}; - -@Injectable() -export class ViewCollectionCodeStore extends ComponentStore { - readonly code$ = this.select(({ code }) => code); - readonly editorOptions$ = this.select(({ editorOptions }) => editorOptions); - - constructor( - collectionStore: CollectionStore, - collectionAttributesStore: CollectionAttributesStore, - darkThemeStore: DarkThemeStore - ) { - super(initialState); - - this._loadEditorOptions(darkThemeStore.isDarkThemeEnabled$); - this._loadCode( - this.select( - collectionStore.collection$, - collectionAttributesStore.collectionAttributes$, - (collection, collectionAttributes) => ({ - collection: collection?.document ?? null, - collectionAttributes: collectionAttributes.map( - ({ document }) => document - ), - }), - { debounce: true } - ) - ); - } - - private readonly _loadEditorOptions = this.updater( - (state, isDarkThemeEnabled) => ({ - ...state, - editorOptions: { - theme: isDarkThemeEnabled ? 'vs-dark' : 'vs-light', - language: 'rust', - automaticLayout: true, - readOnly: true, - fontSize: 16, - }, - }) - ); - - private readonly _loadCode = this.updater<{ - collection: Document | null; - collectionAttributes: Document[]; - }>((state, { collection, collectionAttributes }) => ({ - ...state, - code: - collection && generateCollectionCode(collection, collectionAttributes), - })); -} diff --git a/libs/bulldozer/client/collections/pages/view-collection/src/lib/view-collection-tab.component.ts b/libs/bulldozer/client/collections/pages/view-collection/src/lib/view-collection-tab.component.ts new file mode 100644 index 00000000..5c24fd38 --- /dev/null +++ b/libs/bulldozer/client/collections/pages/view-collection/src/lib/view-collection-tab.component.ts @@ -0,0 +1,83 @@ +import { Component, HostBinding, Input } from '@angular/core'; +import { CollectionStore } from '@bulldozer-client/collections-data-access'; +import { TabStore } from '@bulldozer-client/core-data-access'; +import { ViewCollectionStore } from './view-collection.store'; + +@Component({ + selector: 'bd-view-collection-tab', + template: ` + + `, + providers: [CollectionStore, ViewCollectionStore], +}) +export class ViewCollectionTabComponent { + @HostBinding('class') class = 'block w-full'; + + @Input() url: string | null = null; + @Input() set collectionId(value: string) { + this._viewCollectionStore.setCollectionId(value); + } + + readonly collectionId$ = this._viewCollectionStore.collectionId$; + readonly collection$ = this._viewCollectionStore.collection$; + readonly loading$ = this._collectionStore.loading$; + + constructor( + private readonly _tabStore: TabStore, + private readonly _collectionStore: CollectionStore, + private readonly _viewCollectionStore: ViewCollectionStore + ) {} + + onCloseTab(collectionId: string) { + this._tabStore.closeTab(collectionId); + } +} diff --git a/libs/bulldozer/client/collections/components/collection-tab/src/lib/collection-tab.module.ts b/libs/bulldozer/client/collections/pages/view-collection/src/lib/view-collection-tab.module.ts similarity index 78% rename from libs/bulldozer/client/collections/components/collection-tab/src/lib/collection-tab.module.ts rename to libs/bulldozer/client/collections/pages/view-collection/src/lib/view-collection-tab.module.ts index 300ee4ce..11795300 100644 --- a/libs/bulldozer/client/collections/components/collection-tab/src/lib/collection-tab.module.ts +++ b/libs/bulldozer/client/collections/pages/view-collection/src/lib/view-collection-tab.module.ts @@ -7,10 +7,10 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; import { ItemUpdatingModule } from '@bulldozer-client/item-updating'; import { ReactiveComponentModule } from '@ngrx/component'; -import { CollectionTabComponent } from './collection-tab.component'; +import { ViewCollectionTabComponent } from './view-collection-tab.component'; @NgModule({ - declarations: [CollectionTabComponent], + declarations: [ViewCollectionTabComponent], imports: [ CommonModule, RouterModule, @@ -21,6 +21,6 @@ import { CollectionTabComponent } from './collection-tab.component'; ReactiveComponentModule, ItemUpdatingModule, ], - exports: [CollectionTabComponent], + exports: [ViewCollectionTabComponent], }) -export class CollectionTabModule {} +export class ViewCollectionTabModule {} diff --git a/libs/bulldozer/client/collections/pages/view-collection/src/lib/view-collection.component.ts b/libs/bulldozer/client/collections/pages/view-collection/src/lib/view-collection.component.ts index 0380563e..50f755c9 100644 --- a/libs/bulldozer/client/collections/pages/view-collection/src/lib/view-collection.component.ts +++ b/libs/bulldozer/client/collections/pages/view-collection/src/lib/view-collection.component.ts @@ -4,165 +4,319 @@ import { HostBinding, OnInit, } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { - CollectionAttributeQueryStore, - CollectionAttributesStore, + CollectionApiService, CollectionStore, } from '@bulldozer-client/collections-data-access'; -import { CollectionAttributeDto } from '@heavy-duty/bulldozer-devkit'; -import { WalletStore } from '@heavy-duty/wallet-adapter'; -import { map } from 'rxjs'; -import { ViewCollectionCodeStore } from './view-collection-code.store'; +import { TabStore } from '@bulldozer-client/core-data-access'; +import { NotificationStore } from '@bulldozer-client/notifications-data-access'; +import { HdBroadcasterSocketStore } from '@heavy-duty/broadcaster'; +import { CollectionDto } from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { combineLatest, distinctUntilChanged, map } from 'rxjs'; import { ViewCollectionStore } from './view-collection.store'; @Component({ selector: 'bd-view-collection', template: ` -
-
-
-

- - {{ collection.document.name }} - - -

-

Visualize all the details about this collection.

+ +
-
- -
+ + + +
+
+
+
+
+
+ + + + + +
+ pipe +
+ +
+
`, styles: [], changeDetection: ChangeDetectionStrategy.OnPush, - providers: [ - CollectionStore, - CollectionAttributesStore, - CollectionAttributeQueryStore, - ViewCollectionStore, - ViewCollectionCodeStore, - ], + providers: [CollectionStore, ViewCollectionStore], }) export class ViewCollectionComponent implements OnInit { - @HostBinding('class') class = 'block'; - readonly connected$ = this._walletStore.connected$; - readonly collection$ = this._collectionStore.collection$; - readonly collectionAttributes$ = - this._collectionAttributesStore.collectionAttributes$; - readonly code$ = this._viewCollectionCodeStore.code$; - readonly editorOptions$ = this._viewCollectionCodeStore.editorOptions$; + @HostBinding('class') class = 'flex h-full'; + readonly collection$ = this._viewCollectionStore.collection$; + readonly loading$ = this._collectionStore.loading$; + readonly workspaceId$ = this._route.paramMap.pipe( + map((paramMap) => paramMap.get('workspaceId')), + isNotNullOrUndefined, + distinctUntilChanged() + ); + readonly applicationId$ = this._route.paramMap.pipe( + map((paramMap) => paramMap.get('applicationId')), + isNotNullOrUndefined, + distinctUntilChanged() + ); + readonly collectionId$ = this._route.paramMap.pipe( + map((paramMap) => paramMap.get('collectionId')), + isNotNullOrUndefined, + distinctUntilChanged() + ); constructor( + private readonly _router: Router, private readonly _route: ActivatedRoute, - private readonly _viewCollectionStore: ViewCollectionStore, - private readonly _viewCollectionCodeStore: ViewCollectionCodeStore, + private readonly _tabStore: TabStore, + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _notificationStore: NotificationStore, + private readonly _collectionApiService: CollectionApiService, private readonly _collectionStore: CollectionStore, - private readonly _collectionAttributesStore: CollectionAttributesStore, - private readonly _walletStore: WalletStore + private readonly _viewCollectionStore: ViewCollectionStore ) {} ngOnInit() { - this._viewCollectionStore.setWorkspaceId( - this._route.paramMap.pipe(map((paramMap) => paramMap.get('workspaceId'))) - ); - this._viewCollectionStore.setApplicationId( - this._route.paramMap.pipe( - map((paramMap) => paramMap.get('applicationId')) + this._viewCollectionStore.setCollectionId(this.collectionId$); + this._tabStore.openTab( + combineLatest({ + workspaceId: this.workspaceId$, + applicationId: this.applicationId$, + collectionId: this.collectionId$, + }).pipe( + map(({ collectionId, applicationId, workspaceId }) => ({ + id: collectionId, + kind: 'collection', + url: `/workspaces/${workspaceId}/applications/${applicationId}/collections/${collectionId}`, + })) ) ); - this._viewCollectionStore.setCollectionId( - this._route.paramMap.pipe(map((paramMap) => paramMap.get('collectionId'))) - ); } - onCreateCollectionAttribute( - workspaceId: string, - applicationId: string, - collectionId: string, - collectionAttributeDto: CollectionAttributeDto - ) { - this._viewCollectionStore.createCollectionAttribute({ - workspaceId, - applicationId, - collectionId, - collectionAttributeDto, + isRouteActive(url: string) { + return this._router.isActive(url, { + paths: 'exact', + queryParams: 'exact', + fragment: 'ignored', + matrixParams: 'ignored', }); } - onUpdateCollectionAttribute( + onUpdateCollection( + authority: string, workspaceId: string, + applicationId: string, collectionId: string, - collectionAttributeId: string, - collectionAttributeDto: CollectionAttributeDto + collectionDto: CollectionDto ) { - this._viewCollectionStore.updateCollectionAttribute({ - workspaceId, - collectionId, - collectionAttributeId, - collectionAttributeDto, - }); + this._collectionApiService + .update({ + authority, + workspaceId, + applicationId, + collectionDto, + collectionId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Update collection request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `applications:${applicationId}:collections`, + `collections:${collectionId}`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); } - onDeleteCollectionAttribute( + onDeleteCollection( + authority: string, workspaceId: string, - collectionId: string, - collectionAttributeId: string + applicationId: string, + collectionId: string ) { - this._viewCollectionStore.deleteCollectionAttribute({ - workspaceId, - collectionId, - collectionAttributeId, - }); + this._collectionApiService + .delete({ + authority, + workspaceId, + applicationId, + collectionId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Delete collection request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `applications:${applicationId}:collections`, + `collections:${collectionId}`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); } } diff --git a/libs/bulldozer/client/collections/pages/view-collection/src/lib/view-collection.module.ts b/libs/bulldozer/client/collections/pages/view-collection/src/lib/view-collection.module.ts index 7e088b8f..6ac099b8 100644 --- a/libs/bulldozer/client/collections/pages/view-collection/src/lib/view-collection.module.ts +++ b/libs/bulldozer/client/collections/pages/view-collection/src/lib/view-collection.module.ts @@ -1,12 +1,14 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; -import { CodeEditorModule } from '@bulldozer-client/code-editor'; -import { CollectionAttributesListModule } from '@bulldozer-client/collection-attributes-list'; +import { EditCollectionModule } from '@bulldozer-client/edit-collection'; import { ItemUpdatingModule } from '@bulldozer-client/item-updating'; -import { PageHeaderModule } from '@bulldozer-client/page-header'; +import { HdBroadcasterCdkModule } from '@heavy-duty/broadcaster-cdk'; +import { HdWalletAdapterCdkModule } from '@heavy-duty/wallet-adapter-cdk'; import { ReactiveComponentModule } from '@ngrx/component'; import { ViewCollectionComponent } from './view-collection.component'; @@ -15,15 +17,40 @@ import { ViewCollectionComponent } from './view-collection.component'; imports: [ CommonModule, RouterModule.forChild([ - { path: '', pathMatch: 'full', component: ViewCollectionComponent }, + { + path: '', + component: ViewCollectionComponent, + children: [ + { + path: 'attributes', + loadChildren: () => + import('@bulldozer-client/view-collection-attributes').then( + (m) => m.ViewCollectionAttributesModule + ), + }, + { + path: 'code-viewer', + loadChildren: () => + import('@bulldozer-client/view-collection-code-viewer').then( + (m) => m.ViewCollectionCodeModule + ), + }, + { + path: '', + redirectTo: 'attributes', + }, + ], + }, ]), + MatButtonModule, MatProgressSpinnerModule, + MatSnackBarModule, MatTooltipModule, ReactiveComponentModule, - PageHeaderModule, - CodeEditorModule, - CollectionAttributesListModule, ItemUpdatingModule, + EditCollectionModule, + HdWalletAdapterCdkModule, + HdBroadcasterCdkModule, ], }) export class ViewCollectionModule {} diff --git a/libs/bulldozer/client/collections/pages/view-collection/src/lib/view-collection.store.ts b/libs/bulldozer/client/collections/pages/view-collection/src/lib/view-collection.store.ts index dfa0c07f..05f1073a 100644 --- a/libs/bulldozer/client/collections/pages/view-collection/src/lib/view-collection.store.ts +++ b/libs/bulldozer/client/collections/pages/view-collection/src/lib/view-collection.store.ts @@ -1,274 +1,189 @@ import { Injectable } from '@angular/core'; +import { CollectionStore } from '@bulldozer-client/collections-data-access'; import { - CollectionAttributeApiService, - CollectionAttributeQueryStore, - CollectionAttributesStore, - CollectionStore, -} from '@bulldozer-client/collections-data-access'; -import { TabStore } from '@bulldozer-client/core-data-access'; -import { NotificationStore } from '@bulldozer-client/notifications-data-access'; -import { InstructionStatus } from '@bulldozer-client/users-data-access'; -import { WorkspaceInstructionsStore } from '@bulldozer-client/workspaces-data-access'; -import { CollectionAttributeDto } from '@heavy-duty/bulldozer-devkit'; -import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; -import { WalletStore } from '@heavy-duty/wallet-adapter'; -import { ComponentStore, tapResponse } from '@ngrx/component-store'; + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; import { - combineLatest, - concatMap, - EMPTY, - filter, - of, - pipe, - switchMap, - tap, - withLatestFrom, -} from 'rxjs'; + Collection, + Document, + flattenInstructions, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined, isTruthy } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-instructions'; +import { CollectionItemView } from './types'; + +const documentToView = (document: Document): CollectionItemView => { + return { + id: document.id, + name: document.name, + isCreating: false, + isUpdating: false, + isDeleting: false, + applicationId: document.data.application, + workspaceId: document.data.workspace, + }; +}; interface ViewModel { collectionId: string | null; - applicationId: string | null; - workspaceId: string | null; + transactions: List; } const initialState: ViewModel = { collectionId: null, - applicationId: null, - workspaceId: null, + transactions: List(), }; @Injectable() export class ViewCollectionStore extends ComponentStore { readonly collectionId$ = this.select(({ collectionId }) => collectionId); - readonly applicationId$ = this.select(({ applicationId }) => applicationId); - readonly workspaceId$ = this.select(({ workspaceId }) => workspaceId); + private readonly _topicName$ = this.select( + this.collectionId$.pipe(isNotNullOrUndefined), + (collectionId) => `collections:${collectionId}` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly collection$ = this.select( + this._collectionStore.collection$, + this._instructionStatuses$, + (collection, instructionStatuses) => + instructionStatuses.reduce( + reduceInstructions, + collection === null ? null : documentToView(collection) + ), + { debounce: true } + ); constructor( - private readonly _walletStore: WalletStore, - private readonly _collectionAttributeApiService: CollectionAttributeApiService, - private readonly _collectionStore: CollectionStore, - private readonly _collectionAttributesStore: CollectionAttributesStore, - private readonly _collectionAttributeQueryStore: CollectionAttributeQueryStore, - private readonly _tabStore: TabStore, - private readonly _notificationStore: NotificationStore, - private readonly _workspaceInstructionsStore: WorkspaceInstructionsStore + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _collectionStore: CollectionStore ) { super(initialState); - this._collectionAttributeQueryStore.setFilters( - combineLatest({ - collection: this.collectionId$.pipe(isNotNullOrUndefined), - }) - ); - this._collectionAttributesStore.setCollectionAttributeIds( - this._collectionAttributeQueryStore.collectionAttributeIds$ - ); - - this._collectionStore.setCollectionId(this.collectionId$); - this._openTab( + this._collectionStore.setCollectionId( this.select( - this.collectionId$, - this.applicationId$, - this.workspaceId$, - (collectionId, applicationId, workspaceId) => ({ - collectionId, - applicationId, - workspaceId, - }), - { debounce: true } + this.collectionId$.pipe(isNotNullOrUndefined), + this._hdBroadcasterSocketStore.connected$.pipe(isTruthy), + (collectionId) => collectionId ) ); - this._handleInstruction( - this.collectionId$.pipe( - isNotNullOrUndefined, - switchMap((collectionId) => - this._workspaceInstructionsStore.instruction$.pipe( - filter((instruction) => - instruction.accounts.some( - (account) => - account.name === 'Collection' && - account.pubkey === collectionId - ) - ) - ) - ) + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) ) ); } - readonly setWorkspaceId = this.updater( - (state, workspaceId) => ({ ...state, workspaceId }) + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) ); - readonly setApplicationId = this.updater( - (state, applicationId) => ({ ...state, applicationId }) + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) ); readonly setCollectionId = this.updater( - (state, collectionId) => ({ ...state, collectionId }) - ); - - private readonly _handleInstruction = this.effect( - tap((instructionStatus) => { - switch (instructionStatus.name) { - case 'createCollection': - case 'updateCollection': - case 'deleteCollection': { - this._collectionStore.dispatch(instructionStatus); - break; - } - case 'createCollectionAttribute': - case 'updateCollectionAttribute': - case 'deleteCollectionAttribute': { - this._collectionAttributesStore.dispatch(instructionStatus); - break; - } - default: - break; - } + (state, collectionId) => ({ + ...state, + collectionId, }) ); - private readonly _openTab = this.effect<{ - collectionId: string | null; - applicationId: string | null; - workspaceId: string | null; - }>( - tap(({ collectionId, applicationId, workspaceId }) => { - if ( - collectionId !== null && - applicationId !== null && - workspaceId !== null - ) { - this._tabStore.openTab({ - id: collectionId, - kind: 'collection', - url: `/workspaces/${workspaceId}/applications/${applicationId}/collections/${collectionId}`, - }); + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); } }) ); - readonly createCollectionAttribute = this.effect<{ - workspaceId: string; - applicationId: string; - collectionId: string; - collectionAttributeDto: CollectionAttributeDto; - }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap( - ([ - { workspaceId, applicationId, collectionId, collectionAttributeDto }, - authority, - ]) => { - if (authority === null) { - return EMPTY; - } - - return this._collectionAttributeApiService - .create({ - collectionAttributeDto, - authority: authority.toBase58(), - workspaceId, - applicationId, - collectionId, - }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent( - 'Create attribute request sent' - ), - (error) => this._notificationStore.setError(error) - ) - ); - } - ) - ) - ); - - readonly updateCollectionAttribute = this.effect<{ - workspaceId: string; - collectionId: string; - collectionAttributeId: string; - collectionAttributeDto: CollectionAttributeDto; + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap( - ([ - { - workspaceId, - collectionId, - collectionAttributeId, - collectionAttributeDto, - }, - authority, - ]) => { - if (authority === null) { - return EMPTY; - } + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } - return this._collectionAttributeApiService - .update({ - authority: authority.toBase58(), - workspaceId, - collectionId, - collectionAttributeDto, - collectionAttributeId, - }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent( - 'Update attribute request sent' - ), - (error) => this._notificationStore.setError(error) - ) + this.patchState({ transactions: List() }); + + const correlationId = uuid(); + let subscriptionId: string; + + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } + + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName ); - } - ) - ) - ); - - readonly deleteCollectionAttribute = this.effect<{ - workspaceId: string; - collectionId: string; - collectionAttributeId: string; - }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap( - ([{ workspaceId, collectionId, collectionAttributeId }, authority]) => { - if (authority === null) { - return EMPTY; } - - return this._collectionAttributeApiService - .delete({ - authority: authority.toBase58(), - workspaceId, - collectionAttributeId, - collectionId, - }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent( - 'Delete attribute request sent' - ), - (error) => this._notificationStore.setError(error) - ) - ); - } - ) - ) + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } + }) + ); + }) ); } diff --git a/libs/bulldozer/client/core/data-access/src/lib/config.store.ts b/libs/bulldozer/client/core/data-access/src/lib/config.store.ts index db266f9a..5c42669d 100644 --- a/libs/bulldozer/client/core/data-access/src/lib/config.store.ts +++ b/libs/bulldozer/client/core/data-access/src/lib/config.store.ts @@ -109,7 +109,11 @@ export class ConfigStore extends ComponentStore { ) ); - setWorkspaceId(workspaceId: string | null) { - this._workspaceId.next(workspaceId); + setWorkspaceId(workspaceId: string) { + const currentWorkspaceId = this._workspaceId.getValue(); + + if (currentWorkspaceId !== workspaceId) { + this._workspaceId.next(workspaceId); + } } } diff --git a/libs/bulldozer/client/core/data-access/src/lib/tab.store.ts b/libs/bulldozer/client/core/data-access/src/lib/tab.store.ts index d090f311..72471239 100644 --- a/libs/bulldozer/client/core/data-access/src/lib/tab.store.ts +++ b/libs/bulldozer/client/core/data-access/src/lib/tab.store.ts @@ -17,7 +17,6 @@ import { tap, withLatestFrom, } from 'rxjs'; -import { ConfigStore } from './config.store'; export interface Tab { id: string; @@ -49,16 +48,12 @@ export class TabStore extends ComponentStore { constructor( private readonly _router: Router, - private readonly _configStore: ConfigStore, private readonly _hdSolanaConfigStore: HdSolanaConfigStore, private readonly _userInstructionsStore: UserInstructionsStore, private readonly _workspaceInstructionsStore: WorkspaceInstructionsStore ) { super(initialState); - this._handleActiveWorkspaceChanges( - this._configStore.workspaceId$.pipe(isNotNullOrUndefined) - ); this._handleNetworkChanges(this._hdSolanaConfigStore.selectedNetwork$); this._removeWorkspaceTabOnDelete( merge( @@ -91,15 +86,6 @@ export class TabStore extends ComponentStore { tabs: state.tabs.filter((tab) => tab.id !== tabId), })); - private readonly _removeWorkspaceTabs = this.updater( - (state, workspaceId) => ({ - ...state, - tabs: state.tabs.filter( - (tab) => !tab.url.startsWith(`/workspaces/${workspaceId}`) - ), - }) - ); - private readonly _handleNetworkChanges = this.effect( pipe( distinctUntilChanged(), @@ -113,16 +99,6 @@ export class TabStore extends ComponentStore { ) ); - private readonly _handleActiveWorkspaceChanges = this.effect( - pipe( - distinctUntilChanged(), - pairwise(), - tap(([previousWorkspaceId]) => - this._removeWorkspaceTabs(previousWorkspaceId) - ) - ) - ); - readonly openTab = this.updater((state, newTab) => { const oldTab = state.tabs.find((tab) => tab.id === newTab.id); @@ -136,18 +112,16 @@ export class TabStore extends ComponentStore { readonly closeTab = this.effect( pipe( tap((tabId) => this._removeTab(tabId)), - concatMap(() => - of(null).pipe( - withLatestFrom(this.tabs$, (_, tabs) => - tabs.length > 0 ? tabs[0] : null - ) - ) + concatMap((tabId) => + of(tabId).pipe(withLatestFrom(this.tabs$, this.selected$)) ), - tap((tab) => { - if (tab) { - this._router.navigateByUrl(tab.url); - } else { - this._router.navigate(['/']); + tap(([tabId, tabs, selected]) => { + if (tabId === selected) { + if (tabs.length > 0) { + this._router.navigateByUrl(tabs[0].url); + } else { + this._router.navigate(['/']); + } } }) ) diff --git a/libs/bulldozer/client/collections/components/collection-tab/.eslintrc.json b/libs/bulldozer/client/core/pages/home/.eslintrc.json similarity index 100% rename from libs/bulldozer/client/collections/components/collection-tab/.eslintrc.json rename to libs/bulldozer/client/core/pages/home/.eslintrc.json diff --git a/libs/bulldozer/client/core/pages/home/README.md b/libs/bulldozer/client/core/pages/home/README.md new file mode 100644 index 00000000..720f8afa --- /dev/null +++ b/libs/bulldozer/client/core/pages/home/README.md @@ -0,0 +1,7 @@ +# bulldozer-client-core-pages-home + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test bulldozer-client-core-pages-home` to execute the unit tests. diff --git a/libs/bulldozer/client/users/components/profile-tab/jest.config.js b/libs/bulldozer/client/core/pages/home/jest.config.js similarity index 80% rename from libs/bulldozer/client/users/components/profile-tab/jest.config.js rename to libs/bulldozer/client/core/pages/home/jest.config.js index c8b62a10..6ef7d5f7 100644 --- a/libs/bulldozer/client/users/components/profile-tab/jest.config.js +++ b/libs/bulldozer/client/core/pages/home/jest.config.js @@ -1,5 +1,5 @@ module.exports = { - displayName: 'bulldozer-client-users-components-profile-tab', + displayName: 'bulldozer-client-core-pages-home', preset: '../../../../../../jest.preset.js', setupFilesAfterEnv: ['/src/test-setup.ts'], globals: { @@ -9,7 +9,7 @@ module.exports = { }, }, coverageDirectory: - '../../../../../../coverage/libs/bulldozer/client/users/components/profile-tab', + '../../../../../../coverage/libs/bulldozer/client/core/pages/home', transform: { '^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular', }, diff --git a/libs/bulldozer/client/core/pages/home/project.json b/libs/bulldozer/client/core/pages/home/project.json new file mode 100644 index 00000000..441eb266 --- /dev/null +++ b/libs/bulldozer/client/core/pages/home/project.json @@ -0,0 +1,26 @@ +{ + "projectType": "library", + "root": "libs/bulldozer/client/core/pages/home", + "sourceRoot": "libs/bulldozer/client/core/pages/home/src", + "prefix": "bd", + "targets": { + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/libs/bulldozer/client/core/pages/home"], + "options": { + "jestConfig": "libs/bulldozer/client/core/pages/home/jest.config.js", + "passWithNoTests": true + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "libs/bulldozer/client/core/pages/home/src/**/*.ts", + "libs/bulldozer/client/core/pages/home/src/**/*.html" + ] + } + } + }, + "tags": [] +} diff --git a/libs/bulldozer/client/core/pages/home/src/index.ts b/libs/bulldozer/client/core/pages/home/src/index.ts new file mode 100644 index 00000000..fe97ad57 --- /dev/null +++ b/libs/bulldozer/client/core/pages/home/src/index.ts @@ -0,0 +1 @@ +export * from './lib/home.module'; diff --git a/libs/bulldozer/client/core/pages/home/src/lib/home.component.ts b/libs/bulldozer/client/core/pages/home/src/lib/home.component.ts new file mode 100644 index 00000000..46d0c721 --- /dev/null +++ b/libs/bulldozer/client/core/pages/home/src/lib/home.component.ts @@ -0,0 +1,143 @@ +import { + ChangeDetectionStrategy, + Component, + HostBinding, + OnInit, +} from '@angular/core'; +import { NotificationStore } from '@bulldozer-client/notifications-data-access'; +import { UserApiService, UserStore } from '@bulldozer-client/users-data-access'; +import { HdBroadcasterSocketStore } from '@heavy-duty/broadcaster'; +import { UserDto } from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { WalletStore } from '@heavy-duty/wallet-adapter'; +import { map } from 'rxjs'; +import { HomeStore } from './home.store'; + +@Component({ + selector: 'bd-home', + template: ` + +
+
+ +
BULLDOZER
+
+

+ + Hello + + anon! + + {{ user.name }}! + + + + ... +

+
+ +
+ + +

+ Looking to register and start building? + +

+
+
+ +

+ Bulldozer is a open source low code platform to build Solana programs. + It gives developers the ability to manage their program's ecosystem + through a UI, hiding all the gory details. +

+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+ `, + styles: [], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [UserStore, HomeStore], +}) +export class HomeComponent implements OnInit { + @HostBinding('class') class = 'block min-h-full w-full py-8'; + readonly user$ = this._homeStore.user$; + readonly userId$ = this._userStore.userId$; + readonly loading$ = this._userStore.loading$; + + constructor( + private readonly _walletStore: WalletStore, + private readonly _userApiService: UserApiService, + private readonly _homeStore: HomeStore, + private readonly _userStore: UserStore, + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _notificationStore: NotificationStore + ) {} + + ngOnInit() { + this._homeStore.setAuthority( + this._walletStore.publicKey$.pipe( + isNotNullOrUndefined, + map((publicKey) => publicKey.toBase58()) + ) + ); + } + + onCreateUser(authority: string, userId: string, userDto: UserDto) { + this._userApiService + .create({ + authority, + userDto, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Create user request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [`authority:${authority}`, `user:${userId}`], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); + } +} diff --git a/libs/bulldozer/client/core/pages/home/src/lib/home.module.ts b/libs/bulldozer/client/core/pages/home/src/lib/home.module.ts new file mode 100644 index 00000000..29e83c03 --- /dev/null +++ b/libs/bulldozer/client/core/pages/home/src/lib/home.module.ts @@ -0,0 +1,21 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { EditUserModule } from '@bulldozer-client/edit-user'; +import { HdWalletAdapterCdkModule } from '@heavy-duty/wallet-adapter-cdk'; +import { ReactiveComponentModule } from '@ngrx/component'; +import { HomeComponent } from './home.component'; + +@NgModule({ + imports: [ + CommonModule, + RouterModule.forChild([ + { path: '', pathMatch: 'full', component: HomeComponent }, + ]), + ReactiveComponentModule, + HdWalletAdapterCdkModule, + EditUserModule, + ], + declarations: [HomeComponent], +}) +export class HomeModule {} diff --git a/libs/bulldozer/client/core/pages/home/src/lib/home.store.ts b/libs/bulldozer/client/core/pages/home/src/lib/home.store.ts new file mode 100644 index 00000000..d147725e --- /dev/null +++ b/libs/bulldozer/client/core/pages/home/src/lib/home.store.ts @@ -0,0 +1,189 @@ +import { Injectable } from '@angular/core'; +import { UserStore } from '@bulldozer-client/users-data-access'; +import { + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; +import { + Document, + flattenInstructions, + InstructionStatus, + User, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined, isTruthy } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-instructions'; +import { UserItemView } from './types'; + +const documentToView = (document: Document): UserItemView => { + return { + id: document.id, + name: document.name, + isCreating: false, + isUpdating: false, + isDeleting: false, + userName: document.data.userName, + thumbnailUrl: document.data.thumbnailUrl, + authority: document.data.authority, + createdAt: document.createdAt.toNumber() * 1000, + }; +}; + +interface ViewModel { + authority: string | null; + transactions: List; +} + +const initialState: ViewModel = { + authority: null, + transactions: List(), +}; + +@Injectable() +export class HomeStore extends ComponentStore { + readonly authority$ = this.select(({ authority }) => authority); + private readonly _topicName$ = this.select( + this.authority$.pipe(isNotNullOrUndefined), + (authority) => `authority:${authority}` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly user$ = this.select( + this._userStore.user$, + this._instructionStatuses$, + (user, instructionStatuses) => + instructionStatuses.reduce( + reduceInstructions, + user === null ? null : documentToView(user) + ), + { debounce: true } + ); + + constructor( + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _userStore: UserStore + ) { + super(initialState); + + this._userStore.setAuthority( + this.select( + this.authority$.pipe(isNotNullOrUndefined), + this._hdBroadcasterSocketStore.connected$.pipe(isTruthy), + (authority) => authority + ) + ); + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) + ) + ); + } + + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) + ); + + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) + ); + + readonly setAuthority = this.updater((state, authority) => ({ + ...state, + authority, + })); + + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); + } + }) + ); + + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; + }>( + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } + + this.patchState({ transactions: List() }); + + const correlationId = uuid(); + let subscriptionId: string; + + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } + + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName + ); + } + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } + }) + ); + }) + ); +} diff --git a/libs/bulldozer/client/core/pages/home/src/lib/reduce-instructions.ts b/libs/bulldozer/client/core/pages/home/src/lib/reduce-instructions.ts new file mode 100644 index 00000000..a8a6d341 --- /dev/null +++ b/libs/bulldozer/client/core/pages/home/src/lib/reduce-instructions.ts @@ -0,0 +1,105 @@ +import { InstructionStatus, UserDto } from '@heavy-duty/bulldozer-devkit'; +import { UserItemView } from './types'; + +export const reduceInstructions = ( + item: UserItemView | null, + instruction: InstructionStatus +): UserItemView | null => { + switch (instruction.name) { + case 'createUser': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: UserDto; + }; + const name = data.arguments.name; + const userName = data.arguments.userName; + const thumbnailUrl = data.arguments.thumbnailUrl; + + const authority = instruction.accounts.find( + (account) => account.name === 'Authority' + )?.pubkey; + const userId = instruction.accounts.find( + (account) => account.name === 'User' + )?.pubkey; + + if (authority === undefined || userId === undefined) { + throw new Error('Malformed Create Instruction'); + } + + return { + id: userId, + name, + isCreating: true, + isUpdating: false, + isDeleting: false, + authority, + userName, + thumbnailUrl, + createdAt: Date.now(), + }; + } else if (instruction.transactionStatus.status === 'finalized') { + if (item === null) { + return null; + } else { + return { + ...item, + isCreating: false, + }; + } + } else { + return item; + } + } + case 'updateUser': { + if (item === null) { + return item; + } else if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: UserDto; + }; + const name = data.arguments.name; + const userName = data.arguments.userName; + const thumbnailUrl = data.arguments.thumbnailUrl; + + return { + ...item, + name, + userName, + thumbnailUrl, + isUpdating: true, + }; + } else if (instruction.transactionStatus.status === 'finalized') { + return { + ...item, + isUpdating: false, + }; + } else { + return item; + } + } + case 'deleteUser': + if (item === null) { + return item; + } else if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + return { + ...item, + isDeleting: true, + }; + } else if (instruction.transactionStatus.status === 'finalized') { + return null; + } else { + return item; + } + default: + return item; + } +}; diff --git a/libs/bulldozer/client/core/pages/home/src/lib/types.ts b/libs/bulldozer/client/core/pages/home/src/lib/types.ts new file mode 100644 index 00000000..39fdb329 --- /dev/null +++ b/libs/bulldozer/client/core/pages/home/src/lib/types.ts @@ -0,0 +1,11 @@ +export interface UserItemView { + id: string; + name: string; + isCreating: boolean; + isUpdating: boolean; + isDeleting: boolean; + userName: string; + thumbnailUrl: string; + authority: string; + createdAt: number; +} diff --git a/libs/bulldozer/client/instructions/components/instruction-arguments-list/src/test-setup.ts b/libs/bulldozer/client/core/pages/home/src/test-setup.ts similarity index 100% rename from libs/bulldozer/client/instructions/components/instruction-arguments-list/src/test-setup.ts rename to libs/bulldozer/client/core/pages/home/src/test-setup.ts diff --git a/libs/bulldozer/client/collections/components/collection-tab/tsconfig.json b/libs/bulldozer/client/core/pages/home/tsconfig.json similarity index 100% rename from libs/bulldozer/client/collections/components/collection-tab/tsconfig.json rename to libs/bulldozer/client/core/pages/home/tsconfig.json diff --git a/libs/bulldozer/client/collections/components/collection-tab/tsconfig.lib.json b/libs/bulldozer/client/core/pages/home/tsconfig.lib.json similarity index 100% rename from libs/bulldozer/client/collections/components/collection-tab/tsconfig.lib.json rename to libs/bulldozer/client/core/pages/home/tsconfig.lib.json diff --git a/libs/bulldozer/client/collections/components/collection-tab/tsconfig.spec.json b/libs/bulldozer/client/core/pages/home/tsconfig.spec.json similarity index 100% rename from libs/bulldozer/client/collections/components/collection-tab/tsconfig.spec.json rename to libs/bulldozer/client/core/pages/home/tsconfig.spec.json diff --git a/libs/bulldozer/client/instructions/components/instruction-arguments-list/.browserslistrc b/libs/bulldozer/client/instructions/components/instruction-arguments-list/.browserslistrc deleted file mode 100644 index 4f9ac269..00000000 --- a/libs/bulldozer/client/instructions/components/instruction-arguments-list/.browserslistrc +++ /dev/null @@ -1,16 +0,0 @@ -# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. -# For additional information regarding the format and rule options, please see: -# https://github.com/browserslist/browserslist#queries - -# For the full list of supported browsers by the Angular framework, please see: -# https://angular.io/guide/browser-support - -# You can see what browsers were selected by your queries by running: -# npx browserslist - -last 1 Chrome version -last 1 Firefox version -last 2 Edge major versions -last 2 Safari major versions -last 2 iOS major versions -Firefox ESR diff --git a/libs/bulldozer/client/instructions/components/instruction-arguments-list/README.md b/libs/bulldozer/client/instructions/components/instruction-arguments-list/README.md deleted file mode 100644 index 80199ecc..00000000 --- a/libs/bulldozer/client/instructions/components/instruction-arguments-list/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# bulldozer-client-instructions-components-instruction-arguments-list - -This library was generated with [Nx](https://nx.dev). - -## Running unit tests - -Run `nx test bulldozer-client-instructions-components-instruction-arguments-list` to execute the unit tests. diff --git a/libs/bulldozer/client/instructions/components/instruction-arguments-list/project.json b/libs/bulldozer/client/instructions/components/instruction-arguments-list/project.json deleted file mode 100644 index e0acad41..00000000 --- a/libs/bulldozer/client/instructions/components/instruction-arguments-list/project.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "projectType": "library", - "root": "libs/bulldozer/client/instructions/components/instruction-arguments-list", - "sourceRoot": "libs/bulldozer/client/instructions/components/instruction-arguments-list/src", - "prefix": "bd", - "targets": { - "test": { - "executor": "@nrwl/jest:jest", - "outputs": [ - "coverage/libs/bulldozer/client/instructions/components/instruction-arguments-list" - ], - "options": { - "jestConfig": "libs/bulldozer/client/instructions/components/instruction-arguments-list/jest.config.js", - "passWithNoTests": true - } - }, - "lint": { - "executor": "@nrwl/linter:eslint", - "options": { - "lintFilePatterns": [ - "libs/bulldozer/client/instructions/components/instruction-arguments-list/src/**/*.ts", - "libs/bulldozer/client/instructions/components/instruction-arguments-list/src/**/*.html" - ] - } - } - }, - "tags": [] -} diff --git a/libs/bulldozer/client/instructions/components/instruction-arguments-list/src/index.ts b/libs/bulldozer/client/instructions/components/instruction-arguments-list/src/index.ts deleted file mode 100644 index 7690c3ba..00000000 --- a/libs/bulldozer/client/instructions/components/instruction-arguments-list/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './lib/instruction-arguments-list.module'; diff --git a/libs/bulldozer/client/instructions/components/instruction-arguments-list/src/lib/instruction-arguments-list.component.ts b/libs/bulldozer/client/instructions/components/instruction-arguments-list/src/lib/instruction-arguments-list.component.ts deleted file mode 100644 index 80d4553d..00000000 --- a/libs/bulldozer/client/instructions/components/instruction-arguments-list/src/lib/instruction-arguments-list.component.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - Input, - Output, -} from '@angular/core'; -import { InstructionArgumentItemView } from '@bulldozer-client/instructions-data-access'; -import { InstructionArgumentDto } from '@heavy-duty/bulldozer-devkit'; - -@Component({ - selector: 'bd-instruction-arguments-list', - template: ` - -
-
-

- Arguments - -

-

Visualize the list of arguments and manage them.

-
- - - -
-
- {{ i + 1 }} -
-
-

- - {{ instructionArgument.document.name }} - - -

-

- Type: - - - {{ instructionArgument.document.data.modifier.name }} - - ({{ instructionArgument.document.data.modifier?.size }}) - - of - - - {{ instructionArgument.document.data.kind.name }}. -

-
- - - - - -
-
-
- - -

There's no arguments yet.

-
-
-
- `, - styles: [], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class InstructionArgumentsListComponent { - @Input() connected = false; - @Input() instructionArguments: InstructionArgumentItemView[] | null = null; - @Output() createInstructionArgument = - new EventEmitter(); - @Output() updateInstructionArgument = new EventEmitter<{ - instructionArgumentId: string; - instructionArgumentDto: InstructionArgumentDto; - }>(); - @Output() deleteInstructionArgument = new EventEmitter(); - - onCreateInstructionArgument(instructionArgumentDto: InstructionArgumentDto) { - this.createInstructionArgument.emit(instructionArgumentDto); - } - - onUpdateInstructionArgument( - instructionArgumentId: string, - instructionArgumentDto: InstructionArgumentDto - ) { - this.updateInstructionArgument.emit({ - instructionArgumentId, - instructionArgumentDto, - }); - } - - onDeleteInstructionArgument(instructionArgumentId: string) { - this.deleteInstructionArgument.emit(instructionArgumentId); - } -} diff --git a/libs/bulldozer/client/instructions/components/instruction-documents-list/.browserslistrc b/libs/bulldozer/client/instructions/components/instruction-documents-list/.browserslistrc deleted file mode 100644 index 4f9ac269..00000000 --- a/libs/bulldozer/client/instructions/components/instruction-documents-list/.browserslistrc +++ /dev/null @@ -1,16 +0,0 @@ -# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. -# For additional information regarding the format and rule options, please see: -# https://github.com/browserslist/browserslist#queries - -# For the full list of supported browsers by the Angular framework, please see: -# https://angular.io/guide/browser-support - -# You can see what browsers were selected by your queries by running: -# npx browserslist - -last 1 Chrome version -last 1 Firefox version -last 2 Edge major versions -last 2 Safari major versions -last 2 iOS major versions -Firefox ESR diff --git a/libs/bulldozer/client/instructions/components/instruction-documents-list/README.md b/libs/bulldozer/client/instructions/components/instruction-documents-list/README.md deleted file mode 100644 index 783b68cd..00000000 --- a/libs/bulldozer/client/instructions/components/instruction-documents-list/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# bulldozer-client-instructions-components-instruction-documents-list - -This library was generated with [Nx](https://nx.dev). - -## Running unit tests - -Run `nx test bulldozer-client-instructions-components-instruction-documents-list` to execute the unit tests. diff --git a/libs/bulldozer/client/instructions/components/instruction-documents-list/project.json b/libs/bulldozer/client/instructions/components/instruction-documents-list/project.json deleted file mode 100644 index 4aca7d64..00000000 --- a/libs/bulldozer/client/instructions/components/instruction-documents-list/project.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "projectType": "library", - "root": "libs/bulldozer/client/instructions/components/instruction-documents-list", - "sourceRoot": "libs/bulldozer/client/instructions/components/instruction-documents-list/src", - "prefix": "bd", - "targets": { - "test": { - "executor": "@nrwl/jest:jest", - "outputs": [ - "coverage/libs/bulldozer/client/instructions/components/instruction-documents-list" - ], - "options": { - "jestConfig": "libs/bulldozer/client/instructions/components/instruction-documents-list/jest.config.js", - "passWithNoTests": true - } - }, - "lint": { - "executor": "@nrwl/linter:eslint", - "options": { - "lintFilePatterns": [ - "libs/bulldozer/client/instructions/components/instruction-documents-list/src/**/*.ts", - "libs/bulldozer/client/instructions/components/instruction-documents-list/src/**/*.html" - ] - } - } - }, - "tags": [] -} diff --git a/libs/bulldozer/client/instructions/components/instruction-documents-list/src/index.ts b/libs/bulldozer/client/instructions/components/instruction-documents-list/src/index.ts deleted file mode 100644 index a8a7016e..00000000 --- a/libs/bulldozer/client/instructions/components/instruction-documents-list/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './lib/instruction-documents-list.module'; diff --git a/libs/bulldozer/client/instructions/components/instruction-documents-list/src/lib/instruction-documents-list.component.ts b/libs/bulldozer/client/instructions/components/instruction-documents-list/src/lib/instruction-documents-list.component.ts deleted file mode 100644 index afcf83d7..00000000 --- a/libs/bulldozer/client/instructions/components/instruction-documents-list/src/lib/instruction-documents-list.component.ts +++ /dev/null @@ -1,312 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - Input, - Output, -} from '@angular/core'; -import { - InstructionAccountItemView, - InstructionDocumentItemView, -} from '@bulldozer-client/instructions-data-access'; -import { - Collection, - Document, - InstructionAccountDto, -} from '@heavy-duty/bulldozer-devkit'; - -@Component({ - selector: 'bd-instruction-documents-list', - template: ` - -
-
-

- Documents - -

-

Visualize the list of documents and manage them.

-
- - - -
-
- description -
- -
-

- - - {{ instructionDocument.document.name }} - - - - - ({{ instructionDocument.document.data.modifier.name }}: - space - {{ instructionDocument.document.data.modifier.space }}) - - - ({{ instructionDocument.document.data.modifier.name }}) - - - - -

- -

- Collection: - {{ instructionDocument.collection.document.name }} - {{ - instructionDocument.collection.document.id - | obscureAddress - }} -

-

- Close: - {{ instructionDocument.close.document.name }} ({{ - instructionDocument.close.document.id | obscureAddress - }}) -

-

- Payer: - {{ instructionDocument.payer.document.name }} ({{ - instructionDocument.payer.document.id | obscureAddress - }}) -

- -

Relations

-
    -
  • - - - - {{ relation.extras.to.document.name }} - - - - ({{ relation.document.id | obscureAddress }}) - - - - - - -
  • -
-
-
- - - - - - - -
-
-
- - -

There's no documents yet.

-
-
-
- `, - styles: [], - changeDetection: ChangeDetectionStrategy.OnPush, - providers: [], -}) -export class InstructionDocumentsListComponent { - @Input() connected = false; - @Input() instructionDocuments: InstructionDocumentItemView[] | null = null; - @Input() instructionAccounts: InstructionAccountItemView[] | null = null; - @Input() collections: Document[] | null = null; - @Output() createInstructionDocument = - new EventEmitter(); - @Output() updateInstructionDocument = new EventEmitter<{ - instructionAccountId: string; - instructionAccountDto: InstructionAccountDto; - }>(); - @Output() deleteInstructionDocument = new EventEmitter(); - @Output() createInstructionRelation = new EventEmitter<{ - fromAccountId: string; - toAccountId: string; - }>(); - @Output() deleteInstructionRelation = new EventEmitter<{ - instructionRelationId: string; - fromAccountId: string; - toAccountId: string; - }>(); - - onCreateInstructionDocument(instructionAccountDto: InstructionAccountDto) { - this.createInstructionDocument.emit(instructionAccountDto); - } - - onUpdateInstructionDocument( - instructionAccountId: string, - instructionAccountDto: InstructionAccountDto - ) { - this.updateInstructionDocument.emit({ - instructionAccountId, - instructionAccountDto, - }); - } - - onDeleteInstructionDocument(instructionAccountId: string) { - this.deleteInstructionDocument.emit(instructionAccountId); - } - - onCreateInstructionRelation(fromAccountId: string, toAccountId: string) { - this.createInstructionRelation.emit({ fromAccountId, toAccountId }); - } - - onDeleteInstructionRelation( - instructionRelationId: string, - fromAccountId: string, - toAccountId: string - ) { - this.deleteInstructionRelation.emit({ - instructionRelationId, - fromAccountId, - toAccountId, - }); - } -} diff --git a/libs/bulldozer/client/instructions/components/instruction-explorer/src/lib/instruction-explorer.component.ts b/libs/bulldozer/client/instructions/components/instruction-explorer/src/lib/instruction-explorer.component.ts index 12ca6c77..1e74ae5d 100644 --- a/libs/bulldozer/client/instructions/components/instruction-explorer/src/lib/instruction-explorer.component.ts +++ b/libs/bulldozer/client/instructions/components/instruction-explorer/src/lib/instruction-explorer.component.ts @@ -1,9 +1,15 @@ import { Component, Input } from '@angular/core'; import { + InstructionApiService, InstructionQueryStore, InstructionsStore, } from '@bulldozer-client/instructions-data-access'; +import { NotificationStore } from '@bulldozer-client/notifications-data-access'; +import { HdBroadcasterSocketStore } from '@heavy-duty/broadcaster'; +import { InstructionDto } from '@heavy-duty/bulldozer-devkit'; +import { Keypair } from '@solana/web3.js'; import { InstructionExplorerStore } from './instruction-explorer.store'; +import { InstructionItemView } from './types'; @Component({ selector: 'bd-instruction-explorer', @@ -11,101 +17,138 @@ import { InstructionExplorerStore } from './instruction-explorer.store';
- Instructions - - + Instructions + + + + + + +
- {{ instruction.document.name }} + {{ instruction.name }} - - - - - + + + + + + + +
@@ -117,8 +160,7 @@ import { InstructionExplorerStore } from './instruction-explorer.store'; ], }) export class InstructionExplorerComponent { - @Input() connected = false; - + @Input() disableCreate = false; @Input() set workspaceId(value: string) { this._instructionExplorerStore.setWorkspaceId(value); } @@ -128,48 +170,131 @@ export class InstructionExplorerComponent { readonly workspaceId$ = this._instructionExplorerStore.workspaceId$; readonly applicationId$ = this._instructionExplorerStore.applicationId$; - readonly instructions$ = this._instructionsStore.instructions$; + readonly instructions$ = this._instructionExplorerStore.instructions$; constructor( - private readonly _instructionExplorerStore: InstructionExplorerStore, - private readonly _instructionsStore: InstructionsStore + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _notificationStore: NotificationStore, + private readonly _instructionApiService: InstructionApiService, + private readonly _instructionExplorerStore: InstructionExplorerStore ) {} onCreateInstruction( + authority: string, workspaceId: string, applicationId: string, - instructionName: string + instructionDto: InstructionDto ) { - this._instructionExplorerStore.createInstruction({ - workspaceId, - applicationId, - instructionName, - }); + const instructionKeypair = Keypair.generate(); + + this._instructionApiService + .create(instructionKeypair, { + authority, + workspaceId, + applicationId, + instructionDto, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Create instruction request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `applications:${applicationId}:instructions`, + `instructions:${instructionKeypair.publicKey.toBase58()}`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); } onUpdateInstruction( + authority: string, workspaceId: string, applicationId: string, instructionId: string, - instructionName: string + instructionDto: InstructionDto ) { - this._instructionExplorerStore.updateInstruction({ - workspaceId, - applicationId, - instructionId, - instructionName, - }); + this._instructionApiService + .update({ + authority, + workspaceId, + applicationId, + instructionDto, + instructionId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Update instruction request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `applications:${applicationId}:instructions`, + `instructions:${instructionId}`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); } onDeleteInstruction( + authority: string, workspaceId: string, applicationId: string, instructionId: string ) { - this._instructionExplorerStore.deleteInstruction({ - workspaceId, - applicationId, - instructionId, - }); + this._instructionApiService + .delete({ + authority, + workspaceId, + applicationId, + instructionId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Delete instruction request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `applications:${applicationId}:instructions`, + `instructions:${instructionId}`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); + } + + identify(_: number, instruction: InstructionItemView) { + return instruction.id; } } diff --git a/libs/bulldozer/client/instructions/components/instruction-explorer/src/lib/instruction-explorer.module.ts b/libs/bulldozer/client/instructions/components/instruction-explorer/src/lib/instruction-explorer.module.ts index ac76eace..f607bf43 100644 --- a/libs/bulldozer/client/instructions/components/instruction-explorer/src/lib/instruction-explorer.module.ts +++ b/libs/bulldozer/client/instructions/components/instruction-explorer/src/lib/instruction-explorer.module.ts @@ -11,6 +11,7 @@ import { RouterModule } from '@angular/router'; import { EditInstructionModule } from '@bulldozer-client/edit-instruction'; import { ItemUpdatingModule } from '@bulldozer-client/item-updating'; import { StopPropagationModule } from '@bulldozer-client/stop-propagation'; +import { HdWalletAdapterCdkModule } from '@heavy-duty/wallet-adapter-cdk'; import { ReactiveComponentModule } from '@ngrx/component'; import { InstructionExplorerComponent } from './instruction-explorer.component'; @@ -26,6 +27,7 @@ import { InstructionExplorerComponent } from './instruction-explorer.component'; MatProgressSpinnerModule, MatTooltipModule, ReactiveComponentModule, + HdWalletAdapterCdkModule, StopPropagationModule, EditInstructionModule, ItemUpdatingModule, diff --git a/libs/bulldozer/client/instructions/components/instruction-explorer/src/lib/instruction-explorer.store.ts b/libs/bulldozer/client/instructions/components/instruction-explorer/src/lib/instruction-explorer.store.ts index 60d53cdc..6067647f 100644 --- a/libs/bulldozer/client/instructions/components/instruction-explorer/src/lib/instruction-explorer.store.ts +++ b/libs/bulldozer/client/instructions/components/instruction-explorer/src/lib/instruction-explorer.store.ts @@ -1,78 +1,129 @@ import { Injectable } from '@angular/core'; +import { InstructionsStore } from '@bulldozer-client/instructions-data-access'; import { - InstructionApiService, - InstructionQueryStore, - InstructionsStore, -} from '@bulldozer-client/instructions-data-access'; -import { NotificationStore } from '@bulldozer-client/notifications-data-access'; -import { InstructionStatus } from '@bulldozer-client/users-data-access'; -import { WorkspaceInstructionsStore } from '@bulldozer-client/workspaces-data-access'; -import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; -import { WalletStore } from '@heavy-duty/wallet-adapter'; -import { ComponentStore, tapResponse } from '@ngrx/component-store'; + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; import { - combineLatest, - concatMap, - EMPTY, - filter, - of, - pipe, - switchMap, - tap, - withLatestFrom, -} from 'rxjs'; + Document, + flattenInstructions, + Instruction, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined, isTruthy } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-instructions'; +import { InstructionItemView } from './types'; + +const documentToView = ( + document: Document +): InstructionItemView => { + return { + id: document.id, + name: document.name, + isCreating: false, + isUpdating: false, + isDeleting: false, + applicationId: document.data.application, + workspaceId: document.data.workspace, + }; +}; interface ViewModel { - applicationId: string | null; workspaceId: string | null; + applicationId: string | null; + transactions: List; } const initialState: ViewModel = { - applicationId: null, workspaceId: null, + applicationId: null, + transactions: List(), }; @Injectable() export class InstructionExplorerStore extends ComponentStore { readonly workspaceId$ = this.select(({ workspaceId }) => workspaceId); readonly applicationId$ = this.select(({ applicationId }) => applicationId); + private readonly _topicName$ = this.select( + this.applicationId$.pipe(isNotNullOrUndefined), + (applicationId) => `applications:${applicationId}:instructions` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly instructions$ = this.select( + this._instructionsStore.instructions$, + this._instructionStatuses$, + (instructions, instructionStatuses) => { + if (instructions === null) { + return null; + } + + return instructionStatuses.reduce( + reduceInstructions, + instructions.map(documentToView) + ); + }, + { debounce: true } + ); constructor( - private readonly _instructionApiService: InstructionApiService, - private readonly _instructionQueryStore: InstructionQueryStore, - private readonly _instructionsStore: InstructionsStore, - private readonly _notificationStore: NotificationStore, - private readonly _walletStore: WalletStore, - workspaceInstructionsStore: WorkspaceInstructionsStore + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _instructionsStore: InstructionsStore ) { super(initialState); - this._instructionQueryStore.setFilters( - combineLatest({ - application: this.applicationId$.pipe(isNotNullOrUndefined), - }) - ); - this._instructionsStore.setInstructionIds( - this._instructionQueryStore.instructionIds$ + this._instructionsStore.setFilters( + this.select( + this.applicationId$.pipe(isNotNullOrUndefined), + this._hdBroadcasterSocketStore.connected$.pipe(isTruthy), + (applicationId) => ({ application: applicationId }) + ) ); - this._handleInstruction( - this.applicationId$.pipe( - isNotNullOrUndefined, - switchMap((applicationId) => - workspaceInstructionsStore.instruction$.pipe( - filter((instruction) => - instruction.accounts.some( - (account) => - account.name === 'Application' && - account.pubkey === applicationId - ) - ) - ) - ) + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) ) ); } + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) + ); + + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) + ); + readonly setWorkspaceId = this.updater( (state, workspaceId) => ({ ...state, workspaceId }) ); @@ -81,131 +132,69 @@ export class InstructionExplorerStore extends ComponentStore { (state, applicationId) => ({ ...state, applicationId }) ); - private readonly _handleInstruction = this.effect( - tap((instructionStatus) => { - switch (instructionStatus.name) { - case 'createInstruction': - case 'updateInstruction': - case 'deleteInstruction': { - this._instructionsStore.dispatch(instructionStatus); - break; - } - default: - break; + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); } }) ); - readonly createInstruction = this.effect<{ - workspaceId: string; - applicationId: string; - instructionName: string; + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap( - ([{ instructionName, workspaceId, applicationId }, authority]) => { - if (authority === null) { - return EMPTY; - } + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } - return this._instructionApiService - .create({ - instructionName, - authority: authority.toBase58(), - workspaceId, - applicationId, - }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent( - 'Create instruction request sent' - ), - (error) => this._notificationStore.setError(error) - ) - ); - } - ) - ) - ); + this.patchState({ transactions: List() }); - readonly updateInstruction = this.effect<{ - workspaceId: string; - applicationId: string; - instructionId: string; - instructionName: string; - }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap( - ([ - { workspaceId, applicationId, instructionId, instructionName }, - authority, - ]) => { - if (authority === null) { - return EMPTY; - } + const correlationId = uuid(); + let subscriptionId: string; - return this._instructionApiService - .update({ - authority: authority.toBase58(), - workspaceId, - applicationId, - instructionName, - instructionId, - }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent( - 'Update instruction request sent' - ), - (error) => this._notificationStore.setError(error) - ) - ); - } - ) - ) - ); - - readonly deleteInstruction = this.effect<{ - workspaceId: string; - applicationId: string; - instructionId: string; - }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap( - ([{ workspaceId, instructionId, applicationId }, authority]) => { - if (authority === null) { - return EMPTY; - } + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } - return this._instructionApiService - .delete({ - authority: authority.toBase58(), - workspaceId, - applicationId, - instructionId, - }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent( - 'Delete instruction request sent' - ), - (error) => this._notificationStore.setError(error) - ) + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName ); - } - ) - ) + } + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } + }) + ); + }) ); } diff --git a/libs/bulldozer/client/instructions/components/instruction-explorer/src/lib/reduce-instructions.ts b/libs/bulldozer/client/instructions/components/instruction-explorer/src/lib/reduce-instructions.ts new file mode 100644 index 00000000..661e7fea --- /dev/null +++ b/libs/bulldozer/client/instructions/components/instruction-explorer/src/lib/reduce-instructions.ts @@ -0,0 +1,162 @@ +import { InstructionStatus } from '@heavy-duty/bulldozer-devkit'; +import { List } from 'immutable'; +import { InstructionItemView } from './types'; + +export const reduceInstructions = ( + items: List, + instruction: InstructionStatus +): List => { + switch (instruction.name) { + case 'createInstruction': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: { name: string }; + }; + const name = data.arguments.name; + + const workspaceId = instruction.accounts.find( + (account) => account.name === 'Workspace' + )?.pubkey; + const applicationId = instruction.accounts.find( + (account) => account.name === 'Application' + )?.pubkey; + const instructionId = instruction.accounts.find( + (account) => account.name === 'Instruction' + )?.pubkey; + + if ( + workspaceId === undefined || + applicationId === undefined || + instructionId === undefined + ) { + throw new Error('Malformed Create Instruction'); + } + + const itemIndex = items.findIndex((item) => item.id === instructionId); + + if (itemIndex === -1) { + return items.push({ + id: instructionId, + name, + isCreating: true, + isUpdating: false, + isDeleting: false, + applicationId, + workspaceId, + }); + } else { + return items.update(itemIndex, (item) => ({ + ...item, + id: instructionId, + name, + isCreating: true, + isUpdating: false, + isDeleting: false, + applicationId, + workspaceId, + })); + } + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Instruction' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isCreating: false, + }; + }); + } else { + return items; + } + } + case 'updateInstruction': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: { name: string }; + }; + const name = data.arguments.name; + + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Instruction' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + name, + isUpdating: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Instruction' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isUpdating: false, + }; + }); + } else { + return items; + } + } + case 'deleteInstruction': + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Instruction' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isDeleting: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.filter( + (item) => + !instruction.accounts.some( + (account) => + account.name === 'Instruction' && account.pubkey === item.id + ) + ); + } else { + return items; + } + default: + return items; + } +}; diff --git a/libs/bulldozer/client/instructions/components/instruction-explorer/src/lib/types.ts b/libs/bulldozer/client/instructions/components/instruction-explorer/src/lib/types.ts new file mode 100644 index 00000000..548a9a97 --- /dev/null +++ b/libs/bulldozer/client/instructions/components/instruction-explorer/src/lib/types.ts @@ -0,0 +1,9 @@ +export interface InstructionItemView { + id: string; + name: string; + isCreating: boolean; + isUpdating: boolean; + isDeleting: boolean; + applicationId: string; + workspaceId: string; +} diff --git a/libs/bulldozer/client/instructions/components/instruction-signers-list/.browserslistrc b/libs/bulldozer/client/instructions/components/instruction-signers-list/.browserslistrc deleted file mode 100644 index 4f9ac269..00000000 --- a/libs/bulldozer/client/instructions/components/instruction-signers-list/.browserslistrc +++ /dev/null @@ -1,16 +0,0 @@ -# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. -# For additional information regarding the format and rule options, please see: -# https://github.com/browserslist/browserslist#queries - -# For the full list of supported browsers by the Angular framework, please see: -# https://angular.io/guide/browser-support - -# You can see what browsers were selected by your queries by running: -# npx browserslist - -last 1 Chrome version -last 1 Firefox version -last 2 Edge major versions -last 2 Safari major versions -last 2 iOS major versions -Firefox ESR diff --git a/libs/bulldozer/client/instructions/components/instruction-signers-list/README.md b/libs/bulldozer/client/instructions/components/instruction-signers-list/README.md deleted file mode 100644 index 5f3c2b2d..00000000 --- a/libs/bulldozer/client/instructions/components/instruction-signers-list/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# bulldozer-client-instructions-components-instruction-signers-list - -This library was generated with [Nx](https://nx.dev). - -## Running unit tests - -Run `nx test bulldozer-client-instructions-components-instruction-signers-list` to execute the unit tests. diff --git a/libs/bulldozer/client/instructions/components/instruction-signers-list/project.json b/libs/bulldozer/client/instructions/components/instruction-signers-list/project.json deleted file mode 100644 index cc03c56a..00000000 --- a/libs/bulldozer/client/instructions/components/instruction-signers-list/project.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "projectType": "library", - "root": "libs/bulldozer/client/instructions/components/instruction-signers-list", - "sourceRoot": "libs/bulldozer/client/instructions/components/instruction-signers-list/src", - "prefix": "bd", - "targets": { - "test": { - "executor": "@nrwl/jest:jest", - "outputs": [ - "coverage/libs/bulldozer/client/instructions/components/instruction-signers-list" - ], - "options": { - "jestConfig": "libs/bulldozer/client/instructions/components/instruction-signers-list/jest.config.js", - "passWithNoTests": true - } - }, - "lint": { - "executor": "@nrwl/linter:eslint", - "options": { - "lintFilePatterns": [ - "libs/bulldozer/client/instructions/components/instruction-signers-list/src/**/*.ts", - "libs/bulldozer/client/instructions/components/instruction-signers-list/src/**/*.html" - ] - } - } - }, - "tags": [] -} diff --git a/libs/bulldozer/client/instructions/components/instruction-signers-list/src/index.ts b/libs/bulldozer/client/instructions/components/instruction-signers-list/src/index.ts deleted file mode 100644 index b7c6ccc5..00000000 --- a/libs/bulldozer/client/instructions/components/instruction-signers-list/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './lib/instruction-signers-list.module'; diff --git a/libs/bulldozer/client/instructions/components/instruction-signers-list/src/lib/instruction-signers-list.component.ts b/libs/bulldozer/client/instructions/components/instruction-signers-list/src/lib/instruction-signers-list.component.ts deleted file mode 100644 index 0a90a71e..00000000 --- a/libs/bulldozer/client/instructions/components/instruction-signers-list/src/lib/instruction-signers-list.component.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - Input, - Output, -} from '@angular/core'; -import { InstructionAccountItemView } from '@bulldozer-client/instructions-data-access'; -import { InstructionAccountDto } from '@heavy-duty/bulldozer-devkit'; - -@Component({ - selector: 'bd-instruction-signers-list', - template: ` - -
-
-

- Signers - -

-

Visualize the list of signers and manage them.

-
- - - -
-
- rate_review -
- -

- - - {{ instructionSigner.document.name }} - - - - - ({{ instructionSigner.document.data.modifier.name }} - - - ({{ instructionSigner.document.data.modifier.name }}) - - - - -

- - - - - - -
-
-
- - -

There's no signers yet.

-
-
-
- `, - styles: [], - changeDetection: ChangeDetectionStrategy.OnPush, - providers: [], -}) -export class InstructionSignersListComponent { - @Input() connected = false; - @Input() instructionSigners: InstructionAccountItemView[] | null = null; - @Output() createInstructionSigner = new EventEmitter(); - @Output() updateInstructionSigner = new EventEmitter<{ - instructionAccountId: string; - instructionAccountDto: InstructionAccountDto; - }>(); - @Output() deleteInstructionSigner = new EventEmitter(); - - onCreateInstructionSigner(instructionAccountDto: InstructionAccountDto) { - this.createInstructionSigner.emit(instructionAccountDto); - } - - onUpdateInstructionSigner( - instructionAccountId: string, - instructionAccountDto: InstructionAccountDto - ) { - this.updateInstructionSigner.emit({ - instructionAccountId, - instructionAccountDto, - }); - } - - onDeleteInstructionSigner(instructionAccountId: string) { - this.deleteInstructionSigner.emit(instructionAccountId); - } -} diff --git a/libs/bulldozer/client/instructions/components/instruction-tab/.browserslistrc b/libs/bulldozer/client/instructions/components/instruction-tab/.browserslistrc deleted file mode 100644 index 4f9ac269..00000000 --- a/libs/bulldozer/client/instructions/components/instruction-tab/.browserslistrc +++ /dev/null @@ -1,16 +0,0 @@ -# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. -# For additional information regarding the format and rule options, please see: -# https://github.com/browserslist/browserslist#queries - -# For the full list of supported browsers by the Angular framework, please see: -# https://angular.io/guide/browser-support - -# You can see what browsers were selected by your queries by running: -# npx browserslist - -last 1 Chrome version -last 1 Firefox version -last 2 Edge major versions -last 2 Safari major versions -last 2 iOS major versions -Firefox ESR diff --git a/libs/bulldozer/client/instructions/components/instruction-tab/README.md b/libs/bulldozer/client/instructions/components/instruction-tab/README.md deleted file mode 100644 index 0c89053c..00000000 --- a/libs/bulldozer/client/instructions/components/instruction-tab/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# bulldozer-client-instructions-components-instruction-tab - -This library was generated with [Nx](https://nx.dev). - -## Running unit tests - -Run `nx test bulldozer-client-instructions-components-instruction-tab` to execute the unit tests. diff --git a/libs/bulldozer/client/instructions/components/instruction-tab/project.json b/libs/bulldozer/client/instructions/components/instruction-tab/project.json deleted file mode 100644 index fe73874b..00000000 --- a/libs/bulldozer/client/instructions/components/instruction-tab/project.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "projectType": "library", - "root": "libs/bulldozer/client/instructions/components/instruction-tab", - "sourceRoot": "libs/bulldozer/client/instructions/components/instruction-tab/src", - "prefix": "bd", - "targets": { - "test": { - "executor": "@nrwl/jest:jest", - "outputs": [ - "coverage/libs/bulldozer/client/instructions/components/instruction-tab" - ], - "options": { - "jestConfig": "libs/bulldozer/client/instructions/components/instruction-tab/jest.config.js", - "passWithNoTests": true - } - }, - "lint": { - "executor": "@nrwl/linter:eslint", - "options": { - "lintFilePatterns": [ - "libs/bulldozer/client/instructions/components/instruction-tab/src/**/*.ts", - "libs/bulldozer/client/instructions/components/instruction-tab/src/**/*.html" - ] - } - } - }, - "tags": [] -} diff --git a/libs/bulldozer/client/instructions/components/instruction-tab/src/index.ts b/libs/bulldozer/client/instructions/components/instruction-tab/src/index.ts deleted file mode 100644 index 69c7760d..00000000 --- a/libs/bulldozer/client/instructions/components/instruction-tab/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './lib/instruction-tab.module'; diff --git a/libs/bulldozer/client/instructions/components/instruction-tab/src/lib/instruction-tab.component.ts b/libs/bulldozer/client/instructions/components/instruction-tab/src/lib/instruction-tab.component.ts deleted file mode 100644 index 8c3b05ba..00000000 --- a/libs/bulldozer/client/instructions/components/instruction-tab/src/lib/instruction-tab.component.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Component, HostBinding, Input } from '@angular/core'; -import { TabStore } from '@bulldozer-client/core-data-access'; -import { InstructionStore } from '@bulldozer-client/instructions-data-access'; -import { InstructionTabStore } from './instruction-tab.store'; - -@Component({ - selector: 'bd-instruction-tab', - template: ` - - `, - providers: [InstructionStore, InstructionTabStore], -}) -export class InstructionTabComponent { - @HostBinding('class') class = 'block w-full'; - - @Input() set instructionId(value: string) { - this._instructionTabStore.setInstructionId(value); - } - - readonly instruction$ = this._instructionStore.instruction$; - - constructor( - private readonly _tabStore: TabStore, - private readonly _instructionStore: InstructionStore, - private readonly _instructionTabStore: InstructionTabStore - ) {} - - onCloseTab(instructionId: string) { - this._tabStore.closeTab(instructionId); - } -} diff --git a/libs/bulldozer/client/instructions/components/instruction-tab/src/lib/instruction-tab.store.ts b/libs/bulldozer/client/instructions/components/instruction-tab/src/lib/instruction-tab.store.ts deleted file mode 100644 index 0ae06c17..00000000 --- a/libs/bulldozer/client/instructions/components/instruction-tab/src/lib/instruction-tab.store.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { Injectable } from '@angular/core'; -import { TabStore } from '@bulldozer-client/core-data-access'; -import { InstructionStore } from '@bulldozer-client/instructions-data-access'; -import { InstructionStatus } from '@bulldozer-client/users-data-access'; -import { WorkspaceInstructionsStore } from '@bulldozer-client/workspaces-data-access'; -import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; -import { ComponentStore } from '@ngrx/component-store'; -import { filter, switchMap, tap } from 'rxjs'; - -interface ViewModel { - instructionId: string | null; -} - -const initialState: ViewModel = { - instructionId: null, -}; - -@Injectable() -export class InstructionTabStore extends ComponentStore { - readonly instructionId$ = this.select(({ instructionId }) => instructionId); - - constructor( - private readonly _tabStore: TabStore, - private readonly _instructionStore: InstructionStore, - workspaceInstructionsStore: WorkspaceInstructionsStore - ) { - super(initialState); - - this._instructionStore.setInstructionId(this.instructionId$); - this._handleInstruction( - this.instructionId$.pipe( - isNotNullOrUndefined, - switchMap((instructionId) => - workspaceInstructionsStore.instruction$.pipe( - filter((instruction) => - instruction.accounts.some( - (account) => - account.name === 'Instruction' && - account.pubkey === instructionId - ) - ) - ) - ) - ) - ); - this._handleInstructionDeleted( - this.select( - this.instructionId$.pipe(isNotNullOrUndefined), - workspaceInstructionsStore.instruction$.pipe( - filter( - (instruction) => - instruction.name === 'deleteInstruction' && - instruction.status === 'finalized' - ) - ), - (instructionId, instructionStatus) => ({ - instructionId, - instructionStatus, - }) - ).pipe( - filter(({ instructionId, instructionStatus }) => - instructionStatus.accounts.some( - (account) => - account.name === 'Instruction' && account.pubkey === instructionId - ) - ) - ) - ); - } - - readonly setInstructionId = this.updater( - (state, instructionId) => ({ ...state, instructionId }) - ); - - private readonly _handleInstruction = this.effect( - tap((instructionStatus) => { - switch (instructionStatus.name) { - case 'createInstruction': - case 'updateInstruction': - case 'deleteInstruction': { - this._instructionStore.dispatch(instructionStatus); - break; - } - default: - break; - } - }) - ); - - private readonly _handleInstructionDeleted = this.effect<{ - instructionId: string; - instructionStatus: InstructionStatus; - }>(tap(({ instructionId }) => this._tabStore.closeTab(instructionId))); -} diff --git a/libs/bulldozer/client/instructions/data-access/src/index.ts b/libs/bulldozer/client/instructions/data-access/src/index.ts index 97f04764..007aac88 100644 --- a/libs/bulldozer/client/instructions/data-access/src/index.ts +++ b/libs/bulldozer/client/instructions/data-access/src/index.ts @@ -1,5 +1,11 @@ export * from './lib/instruction-account-api.service'; +export * from './lib/instruction-account-close-api.service'; +export * from './lib/instruction-account-closes.store'; +export * from './lib/instruction-account-collection-api.service'; +export * from './lib/instruction-account-collections.store'; export * from './lib/instruction-account-event.service'; +export * from './lib/instruction-account-payer-api.service'; +export * from './lib/instruction-account-payers.store'; export * from './lib/instruction-account-query.store'; export * from './lib/instruction-accounts.store'; export * from './lib/instruction-api.service'; diff --git a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-api.service.ts b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-api.service.ts index 2911828b..bcb6abda 100644 --- a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-api.service.ts +++ b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-api.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; -import { HdBroadcasterStore } from '@heavy-duty/broadcaster'; import { BULLDOZER_PROGRAM_ID, + clearInstructionAccountClose, createInstructionAccount, createInstructionAccountDocument, CreateInstructionAccountParams, @@ -12,25 +12,37 @@ import { InstructionAccountFilters, instructionAccountQueryBuilder, parseBulldozerError, + setInstructionAccountClose, + setInstructionAccountCollection, + setInstructionAccountPayer, updateInstructionAccount, UpdateInstructionAccountParams, } from '@heavy-duty/bulldozer-devkit'; import { HdSolanaApiService, HdSolanaConfigStore, + KeyedAccountInfo, } from '@heavy-duty/ngx-solana'; import { + addInstructionsToTransaction, addInstructionToTransaction, partiallySignTransaction, } from '@heavy-duty/rx-solana'; -import { Finality, Keypair } from '@solana/web3.js'; +import { + Finality, + Keypair, + Transaction, + TransactionSignature, +} from '@solana/web3.js'; import { catchError, + combineLatest, concatMap, first, + forkJoin, map, Observable, - tap, + of, throwError, } from 'rxjs'; @@ -38,8 +50,7 @@ import { export class InstructionAccountApiService { constructor( private readonly _hdSolanaApiService: HdSolanaApiService, - private readonly _hdSolanaConfigStore: HdSolanaConfigStore, - private readonly _hdBroadcasterStore: HdBroadcasterStore + private readonly _hdSolanaConfigStore: HdSolanaConfigStore ) {} private handleError(error: string) { @@ -75,11 +86,16 @@ export class InstructionAccountApiService { return this._hdSolanaApiService .getAccountInfo(instructionAccountId, commitment) .pipe( - map( - (accountInfo) => - accountInfo && - createInstructionAccountDocument(instructionAccountId, accountInfo) - ) + concatMap((accountInfo) => { + if (accountInfo === null) { + return of(null); + } + + return createInstructionAccountDocument( + instructionAccountId, + accountInfo + ); + }) ); } @@ -87,17 +103,22 @@ export class InstructionAccountApiService { findByIds( instructionAccountIds: string[], commitment: Finality = 'finalized' - ) { + ): Observable[]> { return this._hdSolanaApiService .getMultipleAccounts(instructionAccountIds, { commitment }) .pipe( - map((keyedAccounts) => - keyedAccounts.map( - (keyedAccount) => - keyedAccount && - createInstructionAccountDocument( - keyedAccount.accountId, - keyedAccount.accountInfo + concatMap((keyedAccounts) => + forkJoin( + keyedAccounts + .filter( + (keyedAccount): keyedAccount is KeyedAccountInfo => + keyedAccount !== null + ) + .map((keyedAccount) => + createInstructionAccountDocument( + keyedAccount.accountId, + keyedAccount.accountInfo + ) ) ) ) @@ -105,11 +126,35 @@ export class InstructionAccountApiService { } // create instruction account - create(params: Omit) { - const instructionAccountKeypair = Keypair.generate(); + create( + instructionAccountKeypair: Keypair, + params: Omit + ): Observable<{ + transactionSignature: TransactionSignature; + transaction: Transaction; + }> { + const instructions = [ + this._hdSolanaConfigStore.apiEndpoint$.pipe( + first(), + concatMap((apiEndpoint) => { + if (apiEndpoint === null) { + return throwError(() => 'API endpoint missing'); + } - return this._hdSolanaApiService.createTransaction(params.authority).pipe( - addInstructionToTransaction( + return createInstructionAccount(apiEndpoint, { + ...params, + instructionAccountId: + instructionAccountKeypair.publicKey.toBase58(), + }); + }) + ), + ]; + + const { kind, modifier, collection, close, payer } = + params.instructionAccountDto; + + if (kind === 0 && collection !== null) { + instructions.push( this._hdSolanaConfigStore.apiEndpoint$.pipe( first(), concatMap((apiEndpoint) => { @@ -117,23 +162,66 @@ export class InstructionAccountApiService { return throwError(() => 'API endpoint missing'); } - return createInstructionAccount(apiEndpoint, { + return setInstructionAccountCollection(apiEndpoint, { ...params, instructionAccountId: instructionAccountKeypair.publicKey.toBase58(), + collectionId: collection, }); }) ) - ), + ); + } + + if (modifier === 0 && payer !== null) { + instructions.push( + this._hdSolanaConfigStore.apiEndpoint$.pipe( + first(), + concatMap((apiEndpoint) => { + if (apiEndpoint === null) { + return throwError(() => 'API endpoint missing'); + } + + return setInstructionAccountPayer(apiEndpoint, { + ...params, + instructionAccountId: + instructionAccountKeypair.publicKey.toBase58(), + payer, + }); + }) + ) + ); + } + + if (modifier === 1 && close !== null) { + instructions.push( + this._hdSolanaConfigStore.apiEndpoint$.pipe( + first(), + concatMap((apiEndpoint) => { + if (apiEndpoint === null) { + return throwError(() => 'API endpoint missing'); + } + + return setInstructionAccountClose(apiEndpoint, { + ...params, + instructionAccountId: + instructionAccountKeypair.publicKey.toBase58(), + close, + }); + }) + ) + ); + } + + return this._hdSolanaApiService.createTransaction(params.authority).pipe( + addInstructionsToTransaction(combineLatest(instructions)), partiallySignTransaction(instructionAccountKeypair), concatMap((transaction) => this._hdSolanaApiService.sendTransaction(transaction).pipe( - tap((transactionSignature) => - this._hdBroadcasterStore.sendTransaction( - transactionSignature, - params.workspaceId - ) - ), + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), catchError((error) => this.handleError(error)) ) ) @@ -141,9 +229,28 @@ export class InstructionAccountApiService { } // update instruction account - update(params: UpdateInstructionAccountParams) { - return this._hdSolanaApiService.createTransaction(params.authority).pipe( - addInstructionToTransaction( + update(params: UpdateInstructionAccountParams): Observable<{ + transactionSignature: TransactionSignature; + transaction: Transaction; + }> { + const instructions = [ + this._hdSolanaConfigStore.apiEndpoint$.pipe( + first(), + concatMap((apiEndpoint) => { + if (apiEndpoint === null) { + return throwError(() => 'API endpoint missing'); + } + + return updateInstructionAccount(apiEndpoint, params); + }) + ), + ]; + + const { modifier, close, payer, kind, collection } = + params.instructionAccountDto; + + if (kind === 0 && collection !== null) { + instructions.push( this._hdSolanaConfigStore.apiEndpoint$.pipe( first(), concatMap((apiEndpoint) => { @@ -151,18 +258,69 @@ export class InstructionAccountApiService { return throwError(() => 'API endpoint missing'); } - return updateInstructionAccount(apiEndpoint, params); + return setInstructionAccountCollection(apiEndpoint, { + ...params, + instructionAccountId: params.instructionAccountId, + collectionId: collection, + }); }) ) - ), + ); + } + + if (modifier === 0 && payer !== null) { + instructions.push( + this._hdSolanaConfigStore.apiEndpoint$.pipe( + first(), + concatMap((apiEndpoint) => { + if (apiEndpoint === null) { + return throwError(() => 'API endpoint missing'); + } + + return setInstructionAccountPayer(apiEndpoint, { + ...params, + instructionAccountId: params.instructionAccountId, + payer, + }); + }) + ) + ); + } + + if (modifier === 1) { + instructions.push( + this._hdSolanaConfigStore.apiEndpoint$.pipe( + first(), + concatMap((apiEndpoint) => { + if (apiEndpoint === null) { + return throwError(() => 'API endpoint missing'); + } + + if (close === null) { + return clearInstructionAccountClose(apiEndpoint, { + ...params, + instructionAccountId: params.instructionAccountId, + }); + } + + return setInstructionAccountClose(apiEndpoint, { + ...params, + instructionAccountId: params.instructionAccountId, + close, + }); + }) + ) + ); + } + + return this._hdSolanaApiService.createTransaction(params.authority).pipe( + addInstructionsToTransaction(combineLatest(instructions)), concatMap((transaction) => this._hdSolanaApiService.sendTransaction(transaction).pipe( - tap((transactionSignature) => - this._hdBroadcasterStore.sendTransaction( - transactionSignature, - params.workspaceId - ) - ), + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), catchError((error) => this.handleError(error)) ) ) @@ -170,7 +328,10 @@ export class InstructionAccountApiService { } // delete instruction account - delete(params: DeleteInstructionAccountParams) { + delete(params: DeleteInstructionAccountParams): Observable<{ + transactionSignature: TransactionSignature; + transaction: Transaction; + }> { return this._hdSolanaApiService.createTransaction(params.authority).pipe( addInstructionToTransaction( this._hdSolanaConfigStore.apiEndpoint$.pipe( @@ -186,12 +347,10 @@ export class InstructionAccountApiService { ), concatMap((transaction) => this._hdSolanaApiService.sendTransaction(transaction).pipe( - tap((transactionSignature) => - this._hdBroadcasterStore.sendTransaction( - transactionSignature, - params.workspaceId - ) - ), + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), catchError((error) => this.handleError(error)) ) ) diff --git a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-close-api.service.ts b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-close-api.service.ts new file mode 100644 index 00000000..0b51020a --- /dev/null +++ b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-close-api.service.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@angular/core'; +import { + createInstructionAccountCloseDocument, + Document, + InstructionAccountClose, +} from '@heavy-duty/bulldozer-devkit'; +import { HdSolanaApiService, KeyedAccountInfo } from '@heavy-duty/ngx-solana'; +import { Finality } from '@solana/web3.js'; +import { map, Observable } from 'rxjs'; + +@Injectable({ providedIn: 'root' }) +export class InstructionAccountCloseApiService { + constructor(private readonly _hdSolanaApiService: HdSolanaApiService) {} + + // get instruction account + findById( + instructionAccountCloseId: string, + commitment: Finality = 'finalized' + ): Observable | null> { + return this._hdSolanaApiService + .getAccountInfo(instructionAccountCloseId, commitment) + .pipe( + map((accountInfo) => { + if (accountInfo === null) { + return null; + } + + return createInstructionAccountCloseDocument( + instructionAccountCloseId, + accountInfo + ); + }) + ); + } + + // get instruction accounts + findByIds( + instructionAccountCloseIds: string[], + commitment: Finality = 'finalized' + ): Observable[]> { + return this._hdSolanaApiService + .getMultipleAccounts(instructionAccountCloseIds, { commitment }) + .pipe( + map((keyedAccounts) => + keyedAccounts + .filter( + (keyedAccount): keyedAccount is KeyedAccountInfo => + keyedAccount !== null + ) + .map((keyedAccount) => + createInstructionAccountCloseDocument( + keyedAccount.accountId, + keyedAccount.accountInfo + ) + ) + ) + ); + } +} diff --git a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-closes.store.ts b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-closes.store.ts new file mode 100644 index 00000000..85fc266d --- /dev/null +++ b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-closes.store.ts @@ -0,0 +1,96 @@ +import { Injectable } from '@angular/core'; +import { NotificationStore } from '@bulldozer-client/notifications-data-access'; +import { + Document, + InstructionAccountClose, +} from '@heavy-duty/bulldozer-devkit'; +import { ComponentStore, tapResponse } from '@ngrx/component-store'; +import { List, Map } from 'immutable'; +import { EMPTY, switchMap } from 'rxjs'; +import { InstructionAccountCloseApiService } from './instruction-account-close-api.service'; + +interface ViewModel { + loading: boolean; + instructionAccountCloseIds: List | null; + instructionAccountClosesMap: Map< + string, + Document + > | null; +} + +const initialState: ViewModel = { + loading: false, + instructionAccountCloseIds: null, + instructionAccountClosesMap: null, +}; + +@Injectable() +export class InstructionAccountClosesStore extends ComponentStore { + readonly loading$ = this.select(({ loading }) => loading); + readonly instructionAccountCloseIds$ = this.select( + ({ instructionAccountCloseIds }) => instructionAccountCloseIds + ); + readonly instructionAccountClosesMap$ = this.select( + ({ instructionAccountClosesMap }) => instructionAccountClosesMap + ); + readonly instructionAccountCloses$ = this.select( + this.instructionAccountClosesMap$, + (instructionAccountClosesMap) => + instructionAccountClosesMap === null + ? null + : instructionAccountClosesMap.toList() + ); + + constructor( + private readonly _instructionAccountCloseApiService: InstructionAccountCloseApiService, + private readonly _notificationStore: NotificationStore + ) { + super(initialState); + + this._loadInstructionAccountCloses(this.instructionAccountCloseIds$); + } + + readonly setInstructionAccountCloseIds = this.updater | null>( + (state, instructionAccountCloseIds) => ({ + ...state, + instructionAccountCloseIds, + }) + ); + + private readonly _loadInstructionAccountCloses = + this.effect | null>( + switchMap((instructionAccountCloseIds) => { + if (instructionAccountCloseIds === null) { + return EMPTY; + } + + return this._instructionAccountCloseApiService + .findByIds(instructionAccountCloseIds.toArray()) + .pipe( + tapResponse( + (instructionAccountCloses) => { + this.patchState({ + loading: false, + instructionAccountClosesMap: instructionAccountCloses + .filter( + ( + instructionAccountClose + ): instructionAccountClose is Document => + instructionAccountClose !== null + ) + .reduce( + (instructionAccountClosesMap, instructionAccountClose) => + instructionAccountClosesMap.set( + instructionAccountClose.id, + instructionAccountClose + ), + Map>() + ), + }); + }, + (error) => this._notificationStore.setError({ error }) + ) + ); + }) + ); +} diff --git a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-collection-api.service.ts b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-collection-api.service.ts new file mode 100644 index 00000000..093e0b84 --- /dev/null +++ b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-collection-api.service.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@angular/core'; +import { + createInstructionAccountCollectionDocument, + Document, + InstructionAccountCollection, +} from '@heavy-duty/bulldozer-devkit'; +import { HdSolanaApiService, KeyedAccountInfo } from '@heavy-duty/ngx-solana'; +import { Finality } from '@solana/web3.js'; +import { map, Observable } from 'rxjs'; + +@Injectable({ providedIn: 'root' }) +export class InstructionAccountCollectionApiService { + constructor(private readonly _hdSolanaApiService: HdSolanaApiService) {} + + // get instruction account + findById( + instructionAccountCollectionId: string, + commitment: Finality = 'finalized' + ): Observable | null> { + return this._hdSolanaApiService + .getAccountInfo(instructionAccountCollectionId, commitment) + .pipe( + map((accountInfo) => { + if (accountInfo === null) { + return null; + } + + return createInstructionAccountCollectionDocument( + instructionAccountCollectionId, + accountInfo + ); + }) + ); + } + + // get instruction accounts + findByIds( + instructionAccountCollectionIds: string[], + commitment: Finality = 'finalized' + ): Observable[]> { + return this._hdSolanaApiService + .getMultipleAccounts(instructionAccountCollectionIds, { commitment }) + .pipe( + map((keyedAccounts) => + keyedAccounts + .filter( + (keyedAccount): keyedAccount is KeyedAccountInfo => + keyedAccount !== null + ) + .map((keyedAccount) => + createInstructionAccountCollectionDocument( + keyedAccount.accountId, + keyedAccount.accountInfo + ) + ) + ) + ); + } +} diff --git a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-collections.store.ts b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-collections.store.ts new file mode 100644 index 00000000..e35fb5dc --- /dev/null +++ b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-collections.store.ts @@ -0,0 +1,103 @@ +import { Injectable } from '@angular/core'; +import { NotificationStore } from '@bulldozer-client/notifications-data-access'; +import { + Document, + InstructionAccountCollection, +} from '@heavy-duty/bulldozer-devkit'; +import { ComponentStore, tapResponse } from '@ngrx/component-store'; +import { List, Map } from 'immutable'; +import { EMPTY, switchMap } from 'rxjs'; +import { InstructionAccountCollectionApiService } from './instruction-account-collection-api.service'; + +interface ViewModel { + loading: boolean; + instructionAccountCollectionIds: List | null; + instructionAccountCollectionsMap: Map< + string, + Document + > | null; +} + +const initialState: ViewModel = { + loading: false, + instructionAccountCollectionIds: null, + instructionAccountCollectionsMap: null, +}; + +@Injectable() +export class InstructionAccountCollectionsStore extends ComponentStore { + readonly loading$ = this.select(({ loading }) => loading); + readonly instructionAccountCollectionIds$ = this.select( + ({ instructionAccountCollectionIds }) => instructionAccountCollectionIds + ); + readonly instructionAccountCollectionsMap$ = this.select( + ({ instructionAccountCollectionsMap }) => instructionAccountCollectionsMap + ); + readonly instructionAccountCollections$ = this.select( + this.instructionAccountCollectionsMap$, + (instructionAccountCollectionsMap) => + instructionAccountCollectionsMap === null + ? null + : instructionAccountCollectionsMap.toList() + ); + + constructor( + private readonly _instructionAccountCollectionApiService: InstructionAccountCollectionApiService, + private readonly _notificationStore: NotificationStore + ) { + super(initialState); + + this._loadInstructionAccountCollections( + this.instructionAccountCollectionIds$ + ); + } + + readonly setInstructionAccountCollectionIds = + this.updater | null>( + (state, instructionAccountCollectionIds) => ({ + ...state, + instructionAccountCollectionIds, + }) + ); + + private readonly _loadInstructionAccountCollections = + this.effect | null>( + switchMap((instructionAccountCollectionIds) => { + if (instructionAccountCollectionIds === null) { + return EMPTY; + } + + return this._instructionAccountCollectionApiService + .findByIds(instructionAccountCollectionIds.toArray()) + .pipe( + tapResponse( + (instructionAccountCollections) => { + this.patchState({ + loading: false, + instructionAccountCollectionsMap: + instructionAccountCollections + .filter( + ( + instructionAccountCollection + ): instructionAccountCollection is Document => + instructionAccountCollection !== null + ) + .reduce( + ( + instructionAccountCollectionsMap, + instructionAccountCollection + ) => + instructionAccountCollectionsMap.set( + instructionAccountCollection.id, + instructionAccountCollection + ), + Map>() + ), + }); + }, + (error) => this._notificationStore.setError({ error }) + ) + ); + }) + ); +} diff --git a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-event.service.ts b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-event.service.ts index aa7d7873..92e006a6 100644 --- a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-event.service.ts +++ b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-event.service.ts @@ -1,14 +1,5 @@ import { Injectable } from '@angular/core'; -import { - BULLDOZER_PROGRAM_ID, - createInstructionAccountDocument, - Document, - InstructionAccount, - InstructionAccountFilters, - instructionAccountQueryBuilder, -} from '@heavy-duty/bulldozer-devkit'; import { HdSolanaConnectionStore } from '@heavy-duty/ngx-solana'; -import { concatMap, EMPTY, map, Observable, of } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class InstructionAccountEventService { @@ -16,7 +7,7 @@ export class InstructionAccountEventService { private readonly _hdSolanaConnectionStore: HdSolanaConnectionStore ) {} - instructionAccountChanges( + /* instructionAccountChanges( instructionAccountId: string ): Observable | null> { return this._hdSolanaConnectionStore @@ -31,9 +22,9 @@ export class InstructionAccountEventService { : null ) ); - } + } */ - instructionAccountCreated( + /* instructionAccountCreated( filters: InstructionAccountFilters ): Observable> { const query = instructionAccountQueryBuilder() @@ -61,5 +52,5 @@ export class InstructionAccountEventService { } }) ); - } + } */ } diff --git a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-payer-api.service.ts b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-payer-api.service.ts new file mode 100644 index 00000000..42e1eee5 --- /dev/null +++ b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-payer-api.service.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@angular/core'; +import { + createInstructionAccountPayerDocument, + Document, + InstructionAccountPayer, +} from '@heavy-duty/bulldozer-devkit'; +import { HdSolanaApiService, KeyedAccountInfo } from '@heavy-duty/ngx-solana'; +import { Finality } from '@solana/web3.js'; +import { map, Observable } from 'rxjs'; + +@Injectable({ providedIn: 'root' }) +export class InstructionAccountPayerApiService { + constructor(private readonly _hdSolanaApiService: HdSolanaApiService) {} + + // get instruction account + findById( + instructionAccountPayerId: string, + commitment: Finality = 'finalized' + ): Observable | null> { + return this._hdSolanaApiService + .getAccountInfo(instructionAccountPayerId, commitment) + .pipe( + map((accountInfo) => { + if (accountInfo === null) { + return null; + } + + return createInstructionAccountPayerDocument( + instructionAccountPayerId, + accountInfo + ); + }) + ); + } + + // get instruction accounts + findByIds( + instructionAccountPayerIds: string[], + commitment: Finality = 'finalized' + ): Observable[]> { + return this._hdSolanaApiService + .getMultipleAccounts(instructionAccountPayerIds, { commitment }) + .pipe( + map((keyedAccounts) => + keyedAccounts + .filter( + (keyedAccount): keyedAccount is KeyedAccountInfo => + keyedAccount !== null + ) + .map((keyedAccount) => + createInstructionAccountPayerDocument( + keyedAccount.accountId, + keyedAccount.accountInfo + ) + ) + ) + ); + } +} diff --git a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-payers.store.ts b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-payers.store.ts new file mode 100644 index 00000000..a989b8c4 --- /dev/null +++ b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-account-payers.store.ts @@ -0,0 +1,96 @@ +import { Injectable } from '@angular/core'; +import { NotificationStore } from '@bulldozer-client/notifications-data-access'; +import { + Document, + InstructionAccountPayer, +} from '@heavy-duty/bulldozer-devkit'; +import { ComponentStore, tapResponse } from '@ngrx/component-store'; +import { List, Map } from 'immutable'; +import { EMPTY, switchMap } from 'rxjs'; +import { InstructionAccountPayerApiService } from './instruction-account-payer-api.service'; + +interface ViewModel { + loading: boolean; + instructionAccountPayerIds: List | null; + instructionAccountPayersMap: Map< + string, + Document + > | null; +} + +const initialState: ViewModel = { + loading: false, + instructionAccountPayerIds: null, + instructionAccountPayersMap: null, +}; + +@Injectable() +export class InstructionAccountPayersStore extends ComponentStore { + readonly loading$ = this.select(({ loading }) => loading); + readonly instructionAccountPayerIds$ = this.select( + ({ instructionAccountPayerIds }) => instructionAccountPayerIds + ); + readonly instructionAccountPayersMap$ = this.select( + ({ instructionAccountPayersMap }) => instructionAccountPayersMap + ); + readonly instructionAccountPayers$ = this.select( + this.instructionAccountPayersMap$, + (instructionAccountPayersMap) => + instructionAccountPayersMap === null + ? null + : instructionAccountPayersMap.toList() + ); + + constructor( + private readonly _instructionAccountPayerApiService: InstructionAccountPayerApiService, + private readonly _notificationStore: NotificationStore + ) { + super(initialState); + + this._loadInstructionAccountPayers(this.instructionAccountPayerIds$); + } + + readonly setInstructionAccountPayerIds = this.updater | null>( + (state, instructionAccountPayerIds) => ({ + ...state, + instructionAccountPayerIds, + }) + ); + + private readonly _loadInstructionAccountPayers = + this.effect | null>( + switchMap((instructionAccountPayerIds) => { + if (instructionAccountPayerIds === null) { + return EMPTY; + } + + return this._instructionAccountPayerApiService + .findByIds(instructionAccountPayerIds.toArray()) + .pipe( + tapResponse( + (instructionAccountPayers) => { + this.patchState({ + loading: false, + instructionAccountPayersMap: instructionAccountPayers + .filter( + ( + instructionAccountPayer + ): instructionAccountPayer is Document => + instructionAccountPayer !== null + ) + .reduce( + (instructionAccountPayersMap, instructionAccountPayer) => + instructionAccountPayersMap.set( + instructionAccountPayer.id, + instructionAccountPayer + ), + Map>() + ), + }); + }, + (error) => this._notificationStore.setError({ error }) + ) + ); + }) + ); +} diff --git a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-accounts.store.ts b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-accounts.store.ts index aade0506..71077b06 100644 --- a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-accounts.store.ts +++ b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-accounts.store.ts @@ -1,49 +1,33 @@ import { Injectable } from '@angular/core'; -import { CollectionItemView } from '@bulldozer-client/collections-data-access'; import { NotificationStore } from '@bulldozer-client/notifications-data-access'; -import { InstructionStatus } from '@bulldozer-client/users-data-access'; -import { Document, InstructionAccount } from '@heavy-duty/bulldozer-devkit'; -import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { + Document, + InstructionAccount, + InstructionAccountFilters, +} from '@heavy-duty/bulldozer-devkit'; import { ComponentStore, tapResponse } from '@ngrx/component-store'; -import { concatMap, EMPTY, switchMap } from 'rxjs'; +import { List, Map } from 'immutable'; +import { EMPTY, switchMap } from 'rxjs'; import { InstructionAccountApiService } from './instruction-account-api.service'; -import { InstructionRelationItemView } from './instruction-relations.store'; -import { ItemView } from './types'; - -export type InstructionAccountItemView = ItemView>; - -export type InstructionDocumentRelation = InstructionRelationItemView & { - extras: { - to: InstructionAccountItemView; - }; -}; - -export interface InstructionDocumentItemView { - isCreating: boolean; - isUpdating: boolean; - isDeleting: boolean; - document: Document; - close: InstructionAccountItemView | null; - payer: InstructionAccountItemView | null; - collection: CollectionItemView | null; - relations: InstructionDocumentRelation[]; -} interface ViewModel { loading: boolean; - instructionAccountIds: string[] | null; - instructionAccountsMap: Map; + filters: InstructionAccountFilters | null; + instructionAccountIds: List | null; + instructionAccountsMap: Map> | null; } const initialState: ViewModel = { loading: false, + filters: null, instructionAccountIds: null, - instructionAccountsMap: new Map(), + instructionAccountsMap: null, }; @Injectable() export class InstructionAccountsStore extends ComponentStore { readonly loading$ = this.select(({ loading }) => loading); + readonly filters$ = this.select(({ filters }) => filters); readonly instructionAccountIds$ = this.select( ({ instructionAccountIds }) => instructionAccountIds ); @@ -53,10 +37,11 @@ export class InstructionAccountsStore extends ComponentStore { readonly instructionAccounts$ = this.select( this.instructionAccountsMap$, (instructionAccountsMap) => - Array.from( - instructionAccountsMap, - ([, instructionAccount]) => instructionAccount - ) + instructionAccountsMap === null + ? null + : instructionAccountsMap + .toList() + .sort((a, b) => (b.createdAt.lt(a.createdAt) ? 1 : -1)) ); constructor( @@ -66,67 +51,60 @@ export class InstructionAccountsStore extends ComponentStore { super(initialState); this._loadInstructionAccounts(this.instructionAccountIds$); + this._loadInstructionAccountIds(this.filters$); } - private readonly _setInstructionAccount = - this.updater((state, newInstructionAccount) => { - const instructionAccountsMap = new Map(state.instructionAccountsMap); - instructionAccountsMap.set( - newInstructionAccount.document.id, - newInstructionAccount - ); - - return { - ...state, - instructionAccountsMap, - }; - }); + readonly setFilters = this.updater( + (state, filters) => ({ + ...state, + filters, + instructionAccountIds: null, + instructionAccountsMap: null, + }) + ); - private readonly _patchStatus = this.updater<{ - instructionAccountId: string; - statuses: { - isCreating?: boolean; - isUpdating?: boolean; - isDeleting?: boolean; - }; - }>((state, { instructionAccountId, statuses }) => { - const instructionAccountsMap = new Map(state.instructionAccountsMap); - const instructionAccount = instructionAccountsMap.get(instructionAccountId); + private readonly _loadInstructionAccountIds = + this.effect( + switchMap((filters) => { + if (filters === null) { + return EMPTY; + } - if (instructionAccount === undefined) { - return state; - } + this.patchState({ + loading: true, + instructionAccountsMap: null, + }); - return { - ...state, - instructionAccountsMap: instructionAccountsMap.set(instructionAccountId, { - ...instructionAccount, - ...statuses, - }), - }; - }); - - private readonly _removeInstructionAccount = this.updater( - (state, instructionAccountId) => { - const instructionAccountsMap = new Map(state.instructionAccountsMap); - instructionAccountsMap.delete(instructionAccountId); - return { - ...state, - instructionAccountsMap, - }; - } - ); + return this._instructionAccountApiService.findIds(filters).pipe( + tapResponse( + (instructionAccountIds) => { + this.patchState({ + instructionAccountIds: List(instructionAccountIds), + }); + }, + (error) => this._notificationStore.setError(error) + ) + ); + }) + ); - private readonly _loadInstructionAccounts = this.effect( + private readonly _loadInstructionAccounts = this.effect | null>( switchMap((instructionAccountIds) => { if (instructionAccountIds === null) { return EMPTY; } - this.patchState({ loading: true }); + if (instructionAccountIds.size === 0) { + this.patchState({ + loading: false, + instructionAccountsMap: Map>(), + }); + + return EMPTY; + } return this._instructionAccountApiService - .findByIds(instructionAccountIds) + .findByIds(instructionAccountIds.toArray()) .pipe( tapResponse( (instructionAccounts) => { @@ -141,13 +119,11 @@ export class InstructionAccountsStore extends ComponentStore { ) .reduce( (instructionAccountsMap, instructionAccount) => - instructionAccountsMap.set(instructionAccount.id, { - document: instructionAccount, - isCreating: false, - isUpdating: false, - isDeleting: false, - }), - new Map() + instructionAccountsMap.set( + instructionAccount.id, + instructionAccount + ), + Map>() ), }); }, @@ -156,99 +132,4 @@ export class InstructionAccountsStore extends ComponentStore { ); }) ); - - readonly setInstructionAccountIds = this.updater( - (state, instructionAccountIds) => ({ - ...state, - instructionAccountIds, - }) - ); - - readonly dispatch = this.effect( - concatMap((instructionAccountStatus) => { - const instructionAccountAccountMeta = - instructionAccountStatus.accounts.find( - (account) => account.name === 'Account' - ); - - if (instructionAccountAccountMeta === undefined) { - return EMPTY; - } - - switch (instructionAccountStatus.name) { - case 'createInstructionAccount': { - if (instructionAccountStatus.status === 'finalized') { - this._patchStatus({ - instructionAccountId: instructionAccountAccountMeta.pubkey, - statuses: { - isCreating: false, - }, - }); - - return EMPTY; - } - - return this._instructionAccountApiService - .findById(instructionAccountAccountMeta.pubkey, 'confirmed') - .pipe( - isNotNullOrUndefined, - tapResponse( - (instructionAccount) => - this._setInstructionAccount({ - document: instructionAccount, - isCreating: true, - isUpdating: false, - isDeleting: false, - }), - (error) => this._notificationStore.setError({ error }) - ) - ); - } - case 'updateInstructionAccount': { - if (instructionAccountStatus.status === 'finalized') { - this._patchStatus({ - instructionAccountId: instructionAccountAccountMeta.pubkey, - statuses: { - isUpdating: false, - }, - }); - - return EMPTY; - } - - return this._instructionAccountApiService - .findById(instructionAccountAccountMeta.pubkey, 'confirmed') - .pipe( - isNotNullOrUndefined, - tapResponse( - (instructionAccount) => - this._setInstructionAccount({ - document: instructionAccount, - isCreating: false, - isUpdating: true, - isDeleting: false, - }), - (error) => this._notificationStore.setError({ error }) - ) - ); - } - case 'deleteInstructionAccount': { - if (instructionAccountStatus.status === 'confirmed') { - this._patchStatus({ - instructionAccountId: instructionAccountAccountMeta.pubkey, - statuses: { isDeleting: true }, - }); - } else { - this._removeInstructionAccount( - instructionAccountAccountMeta.pubkey - ); - } - - return EMPTY; - } - default: - return EMPTY; - } - }) - ); } diff --git a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-api.service.ts b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-api.service.ts index 22ac2aca..eeca0616 100644 --- a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-api.service.ts +++ b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-api.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@angular/core'; -import { HdBroadcasterStore } from '@heavy-duty/broadcaster'; import { BULLDOZER_PROGRAM_ID, createInstruction, @@ -25,14 +24,18 @@ import { addInstructionToTransaction, partiallySignTransaction, } from '@heavy-duty/rx-solana'; -import { Finality, Keypair } from '@solana/web3.js'; +import { + Finality, + Keypair, + Transaction, + TransactionSignature, +} from '@solana/web3.js'; import { catchError, concatMap, first, map, Observable, - tap, throwError, } from 'rxjs'; @@ -40,8 +43,7 @@ import { export class InstructionApiService { constructor( private readonly _hdSolanaApiService: HdSolanaApiService, - private readonly _hdSolanaConfigStore: HdSolanaConfigStore, - private readonly _hdBroadcasterStore: HdBroadcasterStore + private readonly _hdSolanaConfigStore: HdSolanaConfigStore ) {} private handleError(error: string) { @@ -55,8 +57,11 @@ export class InstructionApiService { return this._hdSolanaApiService .getProgramAccounts(BULLDOZER_PROGRAM_ID.toBase58(), { ...query, + dataSlice: { + offset: 0, + length: 0, + }, commitment, - dataSlice: { length: 0, offset: 0 }, }) .pipe( map((programAccounts) => programAccounts.map(({ pubkey }) => pubkey)) @@ -97,9 +102,13 @@ export class InstructionApiService { } // create instruction - create(params: Omit) { - const instructionKeypair = Keypair.generate(); - + create( + instructionKeypair: Keypair, + params: Omit + ): Observable<{ + transactionSignature: TransactionSignature; + transaction: Transaction; + }> { return this._hdSolanaApiService.createTransaction(params.authority).pipe( addInstructionToTransaction( this._hdSolanaConfigStore.apiEndpoint$.pipe( @@ -119,12 +128,10 @@ export class InstructionApiService { partiallySignTransaction(instructionKeypair), concatMap((transaction) => this._hdSolanaApiService.sendTransaction(transaction).pipe( - tap((transactionSignature) => - this._hdBroadcasterStore.sendTransaction( - transactionSignature, - params.workspaceId - ) - ), + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), catchError((error) => this.handleError(error)) ) ) @@ -132,7 +139,10 @@ export class InstructionApiService { } // update instruction - update(params: UpdateInstructionParams) { + update(params: UpdateInstructionParams): Observable<{ + transactionSignature: TransactionSignature; + transaction: Transaction; + }> { return this._hdSolanaApiService.createTransaction(params.authority).pipe( addInstructionToTransaction( this._hdSolanaConfigStore.apiEndpoint$.pipe( @@ -148,12 +158,10 @@ export class InstructionApiService { ), concatMap((transaction) => this._hdSolanaApiService.sendTransaction(transaction).pipe( - tap((transactionSignature) => - this._hdBroadcasterStore.sendTransaction( - transactionSignature, - params.workspaceId - ) - ), + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), catchError((error) => this.handleError(error)) ) ) @@ -161,7 +169,10 @@ export class InstructionApiService { } // update instruction body - updateBody(params: UpdateInstructionBodyParams) { + updateBody(params: UpdateInstructionBodyParams): Observable<{ + transactionSignature: TransactionSignature; + transaction: Transaction; + }> { return this._hdSolanaApiService.createTransaction(params.authority).pipe( addInstructionToTransaction( this._hdSolanaConfigStore.apiEndpoint$.pipe( @@ -177,12 +188,10 @@ export class InstructionApiService { ), concatMap((transaction) => this._hdSolanaApiService.sendTransaction(transaction).pipe( - tap((transactionSignature) => - this._hdBroadcasterStore.sendTransaction( - transactionSignature, - params.workspaceId - ) - ), + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), catchError((error) => this.handleError(error)) ) ) @@ -190,7 +199,10 @@ export class InstructionApiService { } // delete instruction - delete(params: DeleteInstructionParams) { + delete(params: DeleteInstructionParams): Observable<{ + transactionSignature: TransactionSignature; + transaction: Transaction; + }> { return this._hdSolanaApiService.createTransaction(params.authority).pipe( addInstructionToTransaction( this._hdSolanaConfigStore.apiEndpoint$.pipe( @@ -206,12 +218,10 @@ export class InstructionApiService { ), concatMap((transaction) => this._hdSolanaApiService.sendTransaction(transaction).pipe( - tap((transactionSignature) => - this._hdBroadcasterStore.sendTransaction( - transactionSignature, - params.workspaceId - ) - ), + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), catchError((error) => this.handleError(error)) ) ) diff --git a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-argument-api.service.ts b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-argument-api.service.ts index ee6dc7c9..c3362424 100644 --- a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-argument-api.service.ts +++ b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-argument-api.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@angular/core'; -import { HdBroadcasterStore } from '@heavy-duty/broadcaster'; import { BULLDOZER_PROGRAM_ID, createInstructionArgument, @@ -23,14 +22,18 @@ import { addInstructionToTransaction, partiallySignTransaction, } from '@heavy-duty/rx-solana'; -import { Finality, Keypair } from '@solana/web3.js'; +import { + Finality, + Keypair, + Transaction, + TransactionSignature, +} from '@solana/web3.js'; import { catchError, concatMap, first, map, Observable, - tap, throwError, } from 'rxjs'; @@ -38,8 +41,7 @@ import { export class InstructionArgumentApiService { constructor( private readonly _hdSolanaApiService: HdSolanaApiService, - private readonly _hdSolanaConfigStore: HdSolanaConfigStore, - private readonly _hdBroadcasterStore: HdBroadcasterStore + private readonly _hdSolanaConfigStore: HdSolanaConfigStore ) {} private handleError(error: string) { @@ -110,7 +112,10 @@ export class InstructionArgumentApiService { // create instruction argument create( params: Omit - ) { + ): Observable<{ + transactionSignature: TransactionSignature; + transaction: Transaction; + }> { const instructionArgumentKeypair = Keypair.generate(); return this._hdSolanaApiService.createTransaction(params.authority).pipe( @@ -133,12 +138,10 @@ export class InstructionArgumentApiService { partiallySignTransaction(instructionArgumentKeypair), concatMap((transaction) => this._hdSolanaApiService.sendTransaction(transaction).pipe( - tap((transactionSignature) => - this._hdBroadcasterStore.sendTransaction( - transactionSignature, - params.workspaceId - ) - ), + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), catchError((error) => this.handleError(error)) ) ) @@ -146,7 +149,10 @@ export class InstructionArgumentApiService { } // update instruction argument - update(params: UpdateInstructionArgumentParams) { + update(params: UpdateInstructionArgumentParams): Observable<{ + transactionSignature: TransactionSignature; + transaction: Transaction; + }> { return this._hdSolanaApiService.createTransaction(params.authority).pipe( addInstructionToTransaction( this._hdSolanaConfigStore.apiEndpoint$.pipe( @@ -162,12 +168,10 @@ export class InstructionArgumentApiService { ), concatMap((transaction) => this._hdSolanaApiService.sendTransaction(transaction).pipe( - tap((transactionSignature) => - this._hdBroadcasterStore.sendTransaction( - transactionSignature, - params.workspaceId - ) - ), + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), catchError((error) => this.handleError(error)) ) ) @@ -175,7 +179,10 @@ export class InstructionArgumentApiService { } // delete instruction argument - delete(params: DeleteInstructionArgumentParams) { + delete(params: DeleteInstructionArgumentParams): Observable<{ + transactionSignature: TransactionSignature; + transaction: Transaction; + }> { return this._hdSolanaApiService.createTransaction(params.authority).pipe( addInstructionToTransaction( this._hdSolanaConfigStore.apiEndpoint$.pipe( @@ -191,12 +198,10 @@ export class InstructionArgumentApiService { ), concatMap((transaction) => this._hdSolanaApiService.sendTransaction(transaction).pipe( - tap((transactionSignature) => - this._hdBroadcasterStore.sendTransaction( - transactionSignature, - params.workspaceId - ) - ), + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), catchError((error) => this.handleError(error)) ) ) diff --git a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-argument-event.service.ts b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-argument-event.service.ts index acbe2aaa..185fbd2d 100644 --- a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-argument-event.service.ts +++ b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-argument-event.service.ts @@ -1,65 +1,140 @@ import { Injectable } from '@angular/core'; +import { WorkspaceInstructionsStore } from '@bulldozer-client/workspaces-data-access'; import { - BULLDOZER_PROGRAM_ID, - createInstructionArgumentDocument, Document, InstructionArgument, InstructionArgumentFilters, - instructionArgumentQueryBuilder, } from '@heavy-duty/bulldozer-devkit'; -import { HdSolanaConnectionStore } from '@heavy-duty/ngx-solana'; -import { concatMap, EMPTY, map, Observable, of } from 'rxjs'; +import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { Finality } from '@solana/web3.js'; +import { concatMap, EMPTY, filter, map, Observable, of } from 'rxjs'; +import { InstructionArgumentApiService } from './instruction-argument-api.service'; @Injectable({ providedIn: 'root' }) export class InstructionArgumentEventService { constructor( - private readonly _hdSolanaConnectionStore: HdSolanaConnectionStore + private readonly _instructionArgumentApiService: InstructionArgumentApiService, + private readonly _workspaceInstructionStore: WorkspaceInstructionsStore ) {} - instructionArgumentChanges( - instructionArgumentId: string - ): Observable | null> { - return this._hdSolanaConnectionStore - .onAccountChange(instructionArgumentId) - .pipe( - map((accountInfo) => - accountInfo.lamports > 0 - ? createInstructionArgumentDocument( - instructionArgumentId, - accountInfo - ) - : null - ) - ); - } + documentCreated(filters: InstructionArgumentFilters) { + return this._workspaceInstructionStore.getInstruction().pipe( + filter( + (instruction) => + instruction.name === 'createInstructionArgument' && + instruction.accounts.some((account) => { + switch (account.name) { + case 'Authority': + return ( + filters.authority === undefined || + account.pubkey === filters.authority + ); + case 'Workspace': + return ( + filters.workspace === undefined || + account.pubkey === filters.workspace + ); + case 'Application': + return ( + filters.application === undefined || + account.pubkey === filters.application + ); + case 'Instruction': + return ( + filters.instruction === undefined || + account.pubkey === filters.instruction + ); + default: + return false; + } + }) + ), + concatMap((instruction) => { + const status = instruction.status; + const documentId = instruction.accounts.find( + (account) => account.name === 'Argument' + )?.pubkey; + + if (documentId === undefined) { + return EMPTY; + } - instructionArgumentCreated( - filters: InstructionArgumentFilters - ): Observable> { - const query = instructionArgumentQueryBuilder() - .where(filters) - .setCommitment('finalized') - .build(); + if (status === 'confirmed') { + return this._instructionArgumentApiService + .findById(documentId, status) + .pipe( + isNotNullOrUndefined, + map((data) => ({ + data, + status, + })) + ); + } else if (status === 'finalized') { + return of({ + data: documentId, + status, + }); + } else { + return EMPTY; + } + }) + ); + } - return this._hdSolanaConnectionStore - .onProgramAccountChange(BULLDOZER_PROGRAM_ID.toBase58(), query) - .pipe( - concatMap(({ account, pubkey }) => { - if (account.lamports === 0) { - return EMPTY; - } else { - const document = createInstructionArgumentDocument(pubkey, account); + documentUpdated(documentId: string): Observable<{ + status: Finality; + data?: Document; + }> { + return this._workspaceInstructionStore.getInstruction().pipe( + filter( + (instruction) => + instruction.name === 'updateInstructionArgument' && + instruction.accounts.some( + (account) => + account.name === 'Argument' && account.pubkey === documentId + ) + ), + concatMap(({ status }) => { + if (status === 'confirmed') { + return this._instructionArgumentApiService + .findById(documentId, status) + .pipe( + isNotNullOrUndefined, + map((data) => ({ + data, + status, + })) + ); + } else if (status === 'finalized') { + return of({ + status, + }); + } else { + return EMPTY; + } + }) + ); + } - if ( - document.updatedAt !== undefined && - document.createdAt.eq(document.updatedAt) - ) { - return of(document); - } else { - return EMPTY; - } - } - }) - ); + documentDeleted(documentId: string): Observable { + return this._workspaceInstructionStore.getInstruction().pipe( + filter( + (instruction) => + instruction.name === 'deleteInstructionArgument' && + instruction.accounts.some( + (account) => + account.name === 'Argument' && account.pubkey === documentId + ) + ), + concatMap(({ status }) => { + if (status === 'confirmed') { + return of(status); + } else if (status === 'finalized') { + return of(status); + } else { + return EMPTY; + } + }) + ); } } diff --git a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-arguments.store.ts b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-arguments.store.ts index 2f927354..071a448a 100644 --- a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-arguments.store.ts +++ b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-arguments.store.ts @@ -1,32 +1,33 @@ import { Injectable } from '@angular/core'; import { NotificationStore } from '@bulldozer-client/notifications-data-access'; -import { InstructionStatus } from '@bulldozer-client/users-data-access'; -import { Document, InstructionArgument } from '@heavy-duty/bulldozer-devkit'; -import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { + Document, + InstructionArgument, + InstructionArgumentFilters, +} from '@heavy-duty/bulldozer-devkit'; import { ComponentStore, tapResponse } from '@ngrx/component-store'; -import { concatMap, EMPTY, switchMap } from 'rxjs'; +import { List, Map } from 'immutable'; +import { EMPTY, switchMap } from 'rxjs'; import { InstructionArgumentApiService } from './instruction-argument-api.service'; -import { ItemView } from './types'; - -export type InstructionArgumentItemView = ItemView< - Document ->; interface ViewModel { loading: boolean; - instructionArgumentIds: string[] | null; - instructionArgumentsMap: Map; + filters: InstructionArgumentFilters | null; + instructionArgumentIds: List | null; + instructionArgumentsMap: Map> | null; } const initialState: ViewModel = { loading: false, + filters: null, instructionArgumentIds: null, - instructionArgumentsMap: new Map(), + instructionArgumentsMap: null, }; @Injectable() export class InstructionArgumentsStore extends ComponentStore { readonly loading$ = this.select(({ loading }) => loading); + readonly filters$ = this.select(({ filters }) => filters); readonly instructionArgumentIds$ = this.select( ({ instructionArgumentIds }) => instructionArgumentIds ); @@ -36,10 +37,11 @@ export class InstructionArgumentsStore extends ComponentStore { readonly instructionArguments$ = this.select( this.instructionArgumentsMap$, (instructionArgumentsMap) => - Array.from( - instructionArgumentsMap, - ([, instructionArgument]) => instructionArgument - ) + instructionArgumentsMap === null + ? null + : instructionArgumentsMap + .toList() + .sort((a, b) => (b.createdAt.lt(a.createdAt) ? 1 : -1)) ); constructor( @@ -49,74 +51,51 @@ export class InstructionArgumentsStore extends ComponentStore { super(initialState); this._loadInstructionArguments(this.instructionArgumentIds$); + this._loadInstructionArgumentIds(this.filters$); } - private readonly _setInstructionArgument = - this.updater( - (state, newInstructionArgument) => { - const instructionArgumentsMap = new Map(state.instructionArgumentsMap); - instructionArgumentsMap.set( - newInstructionArgument.document.id, - newInstructionArgument - ); - - return { - ...state, - instructionArgumentsMap, - }; - } - ); - - private readonly _patchStatus = this.updater<{ - instructionArgumentId: string; - statuses: { - isCreating?: boolean; - isUpdating?: boolean; - isDeleting?: boolean; - }; - }>((state, { instructionArgumentId, statuses }) => { - const instructionArgumentsMap = new Map(state.instructionArgumentsMap); - const instructionArgument = instructionArgumentsMap.get( - instructionArgumentId - ); - - if (instructionArgument === undefined) { - return state; - } - - return { + readonly setFilters = this.updater( + (state, filters) => ({ ...state, - instructionArgumentsMap: instructionArgumentsMap.set( - instructionArgumentId, - { - ...instructionArgument, - ...statuses, + filters, + instructionArgumentIds: null, + instructionArgumentsMap: null, + }) + ); + + private readonly _loadInstructionArgumentIds = + this.effect( + switchMap((filters) => { + if (filters === null) { + return EMPTY; } - ), - }; - }); - private readonly _removeInstructionArgument = this.updater( - (state, instructionArgumentId) => { - const instructionArgumentsMap = new Map(state.instructionArgumentsMap); - instructionArgumentsMap.delete(instructionArgumentId); - return { - ...state, - instructionArgumentsMap, - }; - } - ); + this.patchState({ + loading: true, + instructionArgumentsMap: null, + }); - private readonly _loadInstructionArguments = this.effect( + return this._instructionArgumentApiService.findIds(filters).pipe( + tapResponse( + (instructionArgumentIds) => { + this.patchState({ + instructionArgumentIds: List(instructionArgumentIds), + }); + }, + (error) => this._notificationStore.setError(error) + ) + ); + }) + ); + + private readonly _loadInstructionArguments = this.effect | null>( switchMap((instructionArgumentIds) => { if (instructionArgumentIds === null) { return EMPTY; } - this.patchState({ loading: true }); - return this._instructionArgumentApiService - .findByIds(instructionArgumentIds) + .findByIds(instructionArgumentIds.toArray()) .pipe( tapResponse( (instructionArguments) => { @@ -131,13 +110,11 @@ export class InstructionArgumentsStore extends ComponentStore { ) .reduce( (instructionArgumentsMap, instructionArgument) => - instructionArgumentsMap.set(instructionArgument.id, { - document: instructionArgument, - isCreating: false, - isUpdating: false, - isDeleting: false, - }), - new Map() + instructionArgumentsMap.set( + instructionArgument.id, + instructionArgument + ), + Map>() ), }); }, @@ -146,99 +123,4 @@ export class InstructionArgumentsStore extends ComponentStore { ); }) ); - - readonly setInstructionArgumentIds = this.updater( - (state, instructionArgumentIds) => ({ - ...state, - instructionArgumentIds, - }) - ); - - readonly dispatch = this.effect( - concatMap((instructionArgumentStatus) => { - const instructionArgumentAccountMeta = - instructionArgumentStatus.accounts.find( - (account) => account.name === 'Argument' - ); - - if (instructionArgumentAccountMeta === undefined) { - return EMPTY; - } - - switch (instructionArgumentStatus.name) { - case 'createInstructionArgument': { - if (instructionArgumentStatus.status === 'finalized') { - this._patchStatus({ - instructionArgumentId: instructionArgumentAccountMeta.pubkey, - statuses: { - isCreating: false, - }, - }); - - return EMPTY; - } - - return this._instructionArgumentApiService - .findById(instructionArgumentAccountMeta.pubkey, 'confirmed') - .pipe( - isNotNullOrUndefined, - tapResponse( - (instructionArgument) => - this._setInstructionArgument({ - document: instructionArgument, - isCreating: true, - isUpdating: false, - isDeleting: false, - }), - (error) => this._notificationStore.setError({ error }) - ) - ); - } - case 'updateInstructionArgument': { - if (instructionArgumentStatus.status === 'finalized') { - this._patchStatus({ - instructionArgumentId: instructionArgumentAccountMeta.pubkey, - statuses: { - isUpdating: false, - }, - }); - - return EMPTY; - } - - return this._instructionArgumentApiService - .findById(instructionArgumentAccountMeta.pubkey, 'confirmed') - .pipe( - isNotNullOrUndefined, - tapResponse( - (instructionArgument) => - this._setInstructionArgument({ - document: instructionArgument, - isCreating: false, - isUpdating: true, - isDeleting: false, - }), - (error) => this._notificationStore.setError({ error }) - ) - ); - } - case 'deleteInstructionArgument': { - if (instructionArgumentStatus.status === 'confirmed') { - this._patchStatus({ - instructionArgumentId: instructionArgumentAccountMeta.pubkey, - statuses: { isDeleting: true }, - }); - } else { - this._removeInstructionArgument( - instructionArgumentAccountMeta.pubkey - ); - } - - return EMPTY; - } - default: - return EMPTY; - } - }) - ); } diff --git a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-relation-api.service.ts b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-relation-api.service.ts index fb986bb8..594098a1 100644 --- a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-relation-api.service.ts +++ b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-relation-api.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@angular/core'; -import { HdBroadcasterStore } from '@heavy-duty/broadcaster'; import { BULLDOZER_PROGRAM_ID, createInstructionRelation, @@ -25,7 +24,6 @@ import { first, map, Observable, - tap, throwError, } from 'rxjs'; @@ -33,8 +31,7 @@ import { export class InstructionRelationApiService { constructor( private readonly _hdSolanaApiService: HdSolanaApiService, - private readonly _hdSolanaConfigStore: HdSolanaConfigStore, - private readonly _hdBroadcasterStore: HdBroadcasterStore + private readonly _hdSolanaConfigStore: HdSolanaConfigStore ) {} private handleError(error: string) { @@ -116,12 +113,10 @@ export class InstructionRelationApiService { ), concatMap((transaction) => this._hdSolanaApiService.sendTransaction(transaction).pipe( - tap((transactionSignature) => - this._hdBroadcasterStore.sendTransaction( - transactionSignature, - params.workspaceId - ) - ), + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), catchError((error) => this.handleError(error)) ) ) @@ -145,12 +140,10 @@ export class InstructionRelationApiService { ), concatMap((transaction) => this._hdSolanaApiService.sendTransaction(transaction).pipe( - tap((transactionSignature) => - this._hdBroadcasterStore.sendTransaction( - transactionSignature, - params.workspaceId - ) - ), + map((transactionSignature) => ({ + transactionSignature, + transaction, + })), catchError((error) => this.handleError(error)) ) ) diff --git a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-relations.store.ts b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-relations.store.ts index 253be4ff..3ca65bcc 100644 --- a/libs/bulldozer/client/instructions/data-access/src/lib/instruction-relations.store.ts +++ b/libs/bulldozer/client/instructions/data-access/src/lib/instruction-relations.store.ts @@ -1,32 +1,33 @@ import { Injectable } from '@angular/core'; import { NotificationStore } from '@bulldozer-client/notifications-data-access'; -import { InstructionStatus } from '@bulldozer-client/users-data-access'; -import { InstructionRelation, Relation } from '@heavy-duty/bulldozer-devkit'; -import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { + InstructionRelation, + InstructionRelationFilters, + Relation, +} from '@heavy-duty/bulldozer-devkit'; import { ComponentStore, tapResponse } from '@ngrx/component-store'; -import { concatMap, EMPTY, switchMap } from 'rxjs'; +import { List, Map } from 'immutable'; +import { EMPTY, switchMap } from 'rxjs'; import { InstructionRelationApiService } from './instruction-relation-api.service'; -import { ItemView } from './types'; - -export type InstructionRelationItemView = ItemView< - Relation ->; interface ViewModel { loading: boolean; - instructionRelationIds: string[] | null; - instructionRelationsMap: Map; + filters: InstructionRelationFilters | null; + instructionRelationIds: List | null; + instructionRelationsMap: Map> | null; } const initialState: ViewModel = { loading: false, + filters: null, instructionRelationIds: null, - instructionRelationsMap: new Map(), + instructionRelationsMap: null, }; @Injectable() export class InstructionRelationsStore extends ComponentStore { readonly loading$ = this.select(({ loading }) => loading); + readonly filters$ = this.select(({ filters }) => filters); readonly instructionRelationIds$ = this.select( ({ instructionRelationIds }) => instructionRelationIds ); @@ -36,10 +37,11 @@ export class InstructionRelationsStore extends ComponentStore { readonly instructionRelations$ = this.select( this.instructionRelationsMap$, (instructionRelationsMap) => - Array.from( - instructionRelationsMap, - ([, instructionRelation]) => instructionRelation - ) + instructionRelationsMap === null + ? null + : instructionRelationsMap + .toList() + .sort((a, b) => (b.createdAt.lt(a.createdAt) ? 1 : -1)) ); constructor( @@ -49,74 +51,51 @@ export class InstructionRelationsStore extends ComponentStore { super(initialState); this._loadInstructionRelations(this.instructionRelationIds$); + this._loadInstructionRelationIds(this.filters$); } - private readonly _setInstructionRelation = - this.updater( - (state, newInstructionRelation) => { - const instructionRelationsMap = new Map(state.instructionRelationsMap); - instructionRelationsMap.set( - newInstructionRelation.document.id, - newInstructionRelation - ); - - return { - ...state, - instructionRelationsMap, - }; - } - ); - - private readonly _patchStatus = this.updater<{ - instructionRelationId: string; - statuses: { - isCreating?: boolean; - isUpdating?: boolean; - isDeleting?: boolean; - }; - }>((state, { instructionRelationId, statuses }) => { - const instructionRelationsMap = new Map(state.instructionRelationsMap); - const instructionRelation = instructionRelationsMap.get( - instructionRelationId - ); - - if (instructionRelation === undefined) { - return state; - } - - return { + readonly setFilters = this.updater( + (state, filters) => ({ ...state, - instructionRelationsMap: instructionRelationsMap.set( - instructionRelationId, - { - ...instructionRelation, - ...statuses, + filters, + instructionRelationIds: null, + instructionRelationsMap: null, + }) + ); + + private readonly _loadInstructionRelationIds = + this.effect( + switchMap((filters) => { + if (filters === null) { + return EMPTY; } - ), - }; - }); - private readonly _removeInstructionRelation = this.updater( - (state, instructionRelationId) => { - const instructionRelationsMap = new Map(state.instructionRelationsMap); - instructionRelationsMap.delete(instructionRelationId); - return { - ...state, - instructionRelationsMap, - }; - } - ); + this.patchState({ + loading: true, + instructionRelationsMap: null, + }); + + return this._instructionRelationApiService.findIds(filters).pipe( + tapResponse( + (instructionRelationIds) => { + this.patchState({ + instructionRelationIds: List(instructionRelationIds), + }); + }, + (error) => this._notificationStore.setError(error) + ) + ); + }) + ); - private readonly _loadInstructionRelations = this.effect( + private readonly _loadInstructionRelations = this.effect | null>( switchMap((instructionRelationIds) => { if (instructionRelationIds === null) { return EMPTY; } - this.patchState({ loading: true }); - return this._instructionRelationApiService - .findByIds(instructionRelationIds) + .findByIds(instructionRelationIds.toArray()) .pipe( tapResponse( (instructionRelations) => { @@ -131,13 +110,11 @@ export class InstructionRelationsStore extends ComponentStore { ) .reduce( (instructionRelationsMap, instructionRelation) => - instructionRelationsMap.set(instructionRelation.id, { - document: instructionRelation, - isCreating: false, - isUpdating: false, - isDeleting: false, - }), - new Map() + instructionRelationsMap.set( + instructionRelation.id, + instructionRelation + ), + Map>() ), }); }, @@ -146,71 +123,4 @@ export class InstructionRelationsStore extends ComponentStore { ); }) ); - - readonly setInstructionRelationIds = this.updater( - (state, instructionRelationIds) => ({ - ...state, - instructionRelationIds, - }) - ); - - readonly dispatch = this.effect( - concatMap((instructionRelationStatus) => { - const instructionRelationAccountMeta = - instructionRelationStatus.accounts.find( - (account) => account.name === 'Relation' - ); - - if (instructionRelationAccountMeta === undefined) { - return EMPTY; - } - - switch (instructionRelationStatus.name) { - case 'createInstructionRelation': { - if (instructionRelationStatus.status === 'finalized') { - this._patchStatus({ - instructionRelationId: instructionRelationAccountMeta.pubkey, - statuses: { - isCreating: false, - }, - }); - - return EMPTY; - } - - return this._instructionRelationApiService - .findById(instructionRelationAccountMeta.pubkey, 'confirmed') - .pipe( - isNotNullOrUndefined, - tapResponse( - (instructionRelation) => - this._setInstructionRelation({ - document: instructionRelation, - isCreating: true, - isUpdating: false, - isDeleting: false, - }), - (error) => this._notificationStore.setError({ error }) - ) - ); - } - case 'deleteInstructionRelation': { - if (instructionRelationStatus.status === 'confirmed') { - this._patchStatus({ - instructionRelationId: instructionRelationAccountMeta.pubkey, - statuses: { isDeleting: true }, - }); - } else { - this._removeInstructionRelation( - instructionRelationAccountMeta.pubkey - ); - } - - return EMPTY; - } - default: - return EMPTY; - } - }) - ); } diff --git a/libs/bulldozer/client/instructions/data-access/src/lib/instruction.store.ts b/libs/bulldozer/client/instructions/data-access/src/lib/instruction.store.ts index c2835279..0c29de31 100644 --- a/libs/bulldozer/client/instructions/data-access/src/lib/instruction.store.ts +++ b/libs/bulldozer/client/instructions/data-access/src/lib/instruction.store.ts @@ -1,40 +1,27 @@ import { Injectable } from '@angular/core'; import { NotificationStore } from '@bulldozer-client/notifications-data-access'; -import { InstructionStatus } from '@bulldozer-client/users-data-access'; import { Document, Instruction } from '@heavy-duty/bulldozer-devkit'; import { ComponentStore, tapResponse } from '@ngrx/component-store'; -import { - BehaviorSubject, - combineLatest, - concatMap, - EMPTY, - map, - switchMap, -} from 'rxjs'; +import { EMPTY, switchMap } from 'rxjs'; import { InstructionApiService } from './instruction-api.service'; -import { ItemView } from './types'; - -export type InstructionView = ItemView>; interface ViewModel { - instructionId: string | null; - instruction: InstructionView | null; loading: boolean; + instructionId: string | null; + instruction: Document | null; } const initialState: ViewModel = { + loading: false, instructionId: null, instruction: null, - loading: false, }; @Injectable() export class InstructionStore extends ComponentStore { - private readonly _reload = new BehaviorSubject(null); - private readonly reload$ = this._reload.asObservable(); - readonly instruction$ = this.select(({ instruction }) => instruction); - readonly instructionId$ = this.select(({ instructionId }) => instructionId); readonly loading$ = this.select(({ loading }) => loading); + readonly instructionId$ = this.select(({ instructionId }) => instructionId); + readonly instruction$ = this.select(({ instruction }) => instruction); constructor( private readonly _instructionApiService: InstructionApiService, @@ -42,11 +29,7 @@ export class InstructionStore extends ComponentStore { ) { super(initialState); - this._loadInstruction( - combineLatest([this.instructionId$, this.reload$]).pipe( - map(([instructionId]) => instructionId) - ) - ); + this._loadInstruction(this.instructionId$); } readonly setInstructionId = this.updater( @@ -56,132 +39,25 @@ export class InstructionStore extends ComponentStore { }) ); - private readonly _patchStatus = this.updater<{ - isCreating?: boolean; - isUpdating?: boolean; - isDeleting?: boolean; - }>((state, statuses) => ({ - ...state, - instruction: state.instruction - ? { - ...state.instruction, - ...statuses, - } - : null, - })); - - private readonly _setInstruction = this.updater( - (state, instruction) => ({ - ...state, - instruction, - }) - ); - private readonly _loadInstruction = this.effect( switchMap((instructionId) => { if (instructionId === null) { - this.patchState({ instruction: null }); return EMPTY; } - this.patchState({ loading: true }); + this.patchState({ loading: true, instruction: null }); return this._instructionApiService.findById(instructionId).pipe( tapResponse( (instruction) => { - if (instruction !== null) { - this._setInstruction({ - document: instruction, - isCreating: false, - isUpdating: false, - isDeleting: false, - }); - } - this.patchState({ loading: false }); + this.patchState({ + loading: false, + instruction, + }); }, - (error) => this._notificationStore.setError({ error, loading: false }) + (error) => this._notificationStore.setError({ error }) ) ); }) ); - - readonly dispatch = this.effect( - concatMap((instructionStatus) => { - const instructionAccountMeta = instructionStatus.accounts.find( - (account) => account.name === 'Instruction' - ); - - if (instructionAccountMeta === undefined) { - return EMPTY; - } - - switch (instructionStatus.name) { - case 'createInstruction': { - if (instructionStatus.status === 'finalized') { - this._patchStatus({ isCreating: false }); - return EMPTY; - } - - return this._instructionApiService - .findById(instructionAccountMeta.pubkey, 'confirmed') - .pipe( - tapResponse( - (instruction) => { - if (instruction !== null) { - this._setInstruction({ - document: instruction, - isCreating: true, - isUpdating: false, - isDeleting: false, - }); - } - }, - (error) => this._notificationStore.setError({ error }) - ) - ); - } - case 'updateInstruction': - case 'updateInstructionBody': { - if (instructionStatus.status === 'finalized') { - this._patchStatus({ isUpdating: false }); - return EMPTY; - } - - return this._instructionApiService - .findById(instructionAccountMeta.pubkey, 'confirmed') - .pipe( - tapResponse( - (instruction) => { - if (instruction !== null) { - this._setInstruction({ - document: instruction, - isCreating: false, - isUpdating: true, - isDeleting: false, - }); - } - }, - (error) => this._notificationStore.setError({ error }) - ) - ); - } - case 'deleteInstruction': { - if (instructionStatus.status === 'confirmed') { - this._patchStatus({ isDeleting: true }); - } else { - this.patchState({ instruction: null }); - this._patchStatus({ isDeleting: false }); - } - - return EMPTY; - } - default: - return EMPTY; - } - }) - ); - - reload() { - this._reload.next(null); - } } diff --git a/libs/bulldozer/client/instructions/data-access/src/lib/instructions.store.ts b/libs/bulldozer/client/instructions/data-access/src/lib/instructions.store.ts index d4f61f0a..724b12df 100644 --- a/libs/bulldozer/client/instructions/data-access/src/lib/instructions.store.ts +++ b/libs/bulldozer/client/instructions/data-access/src/lib/instructions.store.ts @@ -1,30 +1,33 @@ import { Injectable } from '@angular/core'; import { NotificationStore } from '@bulldozer-client/notifications-data-access'; -import { InstructionStatus } from '@bulldozer-client/users-data-access'; -import { Document, Instruction } from '@heavy-duty/bulldozer-devkit'; -import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { + Document, + Instruction, + InstructionFilters, +} from '@heavy-duty/bulldozer-devkit'; import { ComponentStore, tapResponse } from '@ngrx/component-store'; -import { concatMap, EMPTY, switchMap } from 'rxjs'; +import { List, Map } from 'immutable'; +import { EMPTY, switchMap } from 'rxjs'; import { InstructionApiService } from './instruction-api.service'; -import { ItemView } from './types'; - -export type InstructionItemView = ItemView>; interface ViewModel { loading: boolean; - instructionIds: string[] | null; - instructionsMap: Map; + filters: InstructionFilters | null; + instructionIds: List | null; + instructionsMap: Map> | null; } const initialState: ViewModel = { loading: false, + filters: null, instructionIds: null, - instructionsMap: new Map(), + instructionsMap: null, }; @Injectable() export class InstructionsStore extends ComponentStore { readonly loading$ = this.select(({ loading }) => loading); + readonly filters$ = this.select(({ filters }) => filters); readonly instructionIds$ = this.select( ({ instructionIds }) => instructionIds ); @@ -34,7 +37,11 @@ export class InstructionsStore extends ComponentStore { readonly instructions$ = this.select( this.instructionsMap$, (instructionsMap) => - Array.from(instructionsMap, ([, instruction]) => instruction) + instructionsMap === null + ? null + : instructionsMap + .toList() + .sort((a, b) => (b.createdAt.lt(a.createdAt) ? 1 : -1)) ); constructor( @@ -44,180 +51,71 @@ export class InstructionsStore extends ComponentStore { super(initialState); this._loadInstructions(this.instructionIds$); + this._loadInstructionIds(this.filters$); } - private readonly _setInstruction = this.updater( - (state, newInstruction) => { - const instructionsMap = new Map(state.instructionsMap); - instructionsMap.set(newInstruction.document.id, newInstruction); - - return { - ...state, - instructionsMap, - }; - } - ); - - private readonly _patchStatus = this.updater<{ - instructionId: string; - statuses: { - isCreating?: boolean; - isUpdating?: boolean; - isDeleting?: boolean; - }; - }>((state, { instructionId, statuses }) => { - const instructionsMap = new Map(state.instructionsMap); - const instruction = instructionsMap.get(instructionId); - - if (instruction === undefined) { - return state; - } - - return { + readonly setFilters = this.updater( + (state, filters) => ({ ...state, - instructionsMap: instructionsMap.set(instructionId, { - ...instruction, - ...statuses, - }), - }; - }); - - private readonly _removeInstruction = this.updater( - (state, instructionId) => { - const instructionsMap = new Map(state.instructionsMap); - instructionsMap.delete(instructionId); - return { - ...state, - instructionsMap, - }; - } + filters, + instructionIds: null, + instructionsMap: null, + }) ); - private readonly _loadInstructions = this.effect( - switchMap((instructionIds) => { - if (instructionIds === null) { + private readonly _loadInstructionIds = this.effect( + switchMap((filters) => { + if (filters === null) { return EMPTY; } - this.patchState({ loading: true }); + this.patchState({ + loading: true, + instructionIds: List(), + instructionsMap: null, + }); - return this._instructionApiService.findByIds(instructionIds).pipe( + return this._instructionApiService.findIds(filters).pipe( tapResponse( - (instructions) => { + (instructionIds) => { this.patchState({ - loading: false, - instructionsMap: instructions - .filter( - (instruction): instruction is Document => - instruction !== null - ) - .reduce( - (instructionsMap, instruction) => - instructionsMap.set(instruction.id, { - document: instruction, - isCreating: false, - isUpdating: false, - isDeleting: false, - }), - new Map() - ), + instructionIds: List(instructionIds), }); }, - (error) => this._notificationStore.setError({ error }) + (error) => this._notificationStore.setError(error) ) ); }) ); - readonly setInstructionIds = this.updater( - (state, instructionIds) => ({ - ...state, - instructionIds, - }) - ); - - readonly dispatch = this.effect( - concatMap((instructionStatus) => { - const instructionAccountMeta = instructionStatus.accounts.find( - (account) => account.name === 'Instruction' - ); - - if (instructionAccountMeta === undefined) { + private readonly _loadInstructions = this.effect | null>( + switchMap((instructionIds) => { + if (instructionIds === null) { return EMPTY; } - switch (instructionStatus.name) { - case 'createInstruction': { - if (instructionStatus.status === 'finalized') { - this._patchStatus({ - instructionId: instructionAccountMeta.pubkey, - statuses: { - isCreating: false, - }, - }); - - return EMPTY; - } - - return this._instructionApiService - .findById(instructionAccountMeta.pubkey, 'confirmed') - .pipe( - isNotNullOrUndefined, - tapResponse( - (instruction) => - this._setInstruction({ - document: instruction, - isCreating: true, - isUpdating: false, - isDeleting: false, - }), - (error) => this._notificationStore.setError({ error }) - ) - ); - } - case 'updateInstruction': { - if (instructionStatus.status === 'finalized') { - this._patchStatus({ - instructionId: instructionAccountMeta.pubkey, - statuses: { - isUpdating: false, - }, - }); - - return EMPTY; - } - - return this._instructionApiService - .findById(instructionAccountMeta.pubkey, 'confirmed') - .pipe( - isNotNullOrUndefined, - tapResponse( - (instruction) => - this._setInstruction({ - document: instruction, - isCreating: false, - isUpdating: true, - isDeleting: false, - }), - (error) => this._notificationStore.setError({ error }) - ) - ); - } - case 'deleteInstruction': { - if (instructionStatus.status === 'confirmed') { - this._patchStatus({ - instructionId: instructionAccountMeta.pubkey, - statuses: { isDeleting: true }, - }); - } else { - this._removeInstruction(instructionAccountMeta.pubkey); - } - - return EMPTY; - } - default: - return EMPTY; - } + return this._instructionApiService + .findByIds(instructionIds.toArray()) + .pipe( + tapResponse( + (instructions) => { + this.patchState({ + loading: false, + instructionsMap: instructions + .filter( + (instruction): instruction is Document => + instruction !== null + ) + .reduce( + (instructionsMap, instruction) => + instructionsMap.set(instruction.id, instruction), + Map>() + ), + }); + }, + (error) => this._notificationStore.setError({ error }) + ) + ); }) ); } diff --git a/libs/bulldozer/client/instructions/modals/edit-instruction-argument/src/lib/edit-instruction-argument.component.ts b/libs/bulldozer/client/instructions/modals/edit-instruction-argument/src/lib/edit-instruction-argument.component.ts index 85e141bc..c46a2c45 100644 --- a/libs/bulldozer/client/instructions/modals/edit-instruction-argument/src/lib/edit-instruction-argument.component.ts +++ b/libs/bulldozer/client/instructions/modals/edit-instruction-argument/src/lib/edit-instruction-argument.component.ts @@ -8,14 +8,15 @@ import { import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { Document, InstructionArgument } from '@heavy-duty/bulldozer-devkit'; +import { SnackBarComponent } from '@bulldozer-client/notification-snack-bar'; +import { InstructionArgumentDto } from '@heavy-duty/bulldozer-devkit'; import { Subject, takeUntil } from 'rxjs'; @Component({ selector: 'bd-edit-argument', template: ` -

- {{ data?.instructionArgument ? 'Edit' : 'Create' }} argument +

+ {{ instructionArgument ? 'Edit' : 'Create' }} argument

- + + +
+
+
+
+
+
+
- - `, }) export class EditInstructionArgumentComponent implements OnInit, OnDestroy { @@ -191,31 +193,19 @@ export class EditInstructionArgumentComponent implements OnInit, OnDestroy { private readonly _matSnackBar: MatSnackBar, private readonly _matDialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) - public data?: { - instructionArgument?: Document; - } + public instructionArgument?: InstructionArgumentDto ) { this.form = new FormGroup({ - name: new FormControl(this.data?.instructionArgument?.name ?? '', { + name: new FormControl(this.instructionArgument?.name ?? '', { validators: [Validators.required], }), - kind: new FormControl(this.data?.instructionArgument?.data.kind.id ?? 0, { + kind: new FormControl(this.instructionArgument?.kind ?? 0, { validators: [Validators.required], }), - modifier: new FormControl( - this.data?.instructionArgument?.data.modifier !== null - ? this.data?.instructionArgument?.data.modifier.id - : null - ), - size: new FormControl( - this.data?.instructionArgument?.data.modifier !== null - ? this.data?.instructionArgument?.data.modifier.size - : null - ), - max: new FormControl(this.data?.instructionArgument?.data.max ?? null), - maxLength: new FormControl( - this.data?.instructionArgument?.data.maxLength ?? null - ), + modifier: new FormControl(this.instructionArgument?.modifier ?? null), + size: new FormControl(this.instructionArgument?.size ?? null), + max: new FormControl(this.instructionArgument?.max ?? null), + maxLength: new FormControl(this.instructionArgument?.maxLength ?? null), }); } @@ -277,9 +267,13 @@ export class EditInstructionArgumentComponent implements OnInit, OnDestroy { if (this.form.valid) { this._matDialogRef.close(this.form.value); } else { - this._matSnackBar.open('Invalid information', 'close', { - panelClass: 'warning-snackbar', + this._matSnackBar.openFromComponent(SnackBarComponent, { duration: 5000, + data: { + title: 'Heey...', + message: 'Invalid Information', + type: 'warning', + }, }); } } diff --git a/libs/bulldozer/client/instructions/modals/edit-instruction-argument/src/lib/edit-instruction-argument-trigger.directive.ts b/libs/bulldozer/client/instructions/modals/edit-instruction-argument/src/lib/edit-instruction-argument.directive.ts similarity index 62% rename from libs/bulldozer/client/instructions/modals/edit-instruction-argument/src/lib/edit-instruction-argument-trigger.directive.ts rename to libs/bulldozer/client/instructions/modals/edit-instruction-argument/src/lib/edit-instruction-argument.directive.ts index 12265960..63218e87 100644 --- a/libs/bulldozer/client/instructions/modals/edit-instruction-argument/src/lib/edit-instruction-argument-trigger.directive.ts +++ b/libs/bulldozer/client/instructions/modals/edit-instruction-argument/src/lib/edit-instruction-argument.directive.ts @@ -6,26 +6,23 @@ import { Output, } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { - Document, - InstructionArgument, - InstructionArgumentDto, -} from '@heavy-duty/bulldozer-devkit'; +import { InstructionArgumentDto } from '@heavy-duty/bulldozer-devkit'; import { EditInstructionArgumentComponent } from './edit-instruction-argument.component'; -@Directive({ selector: '[bdEditInstructionArgumentTrigger]' }) -export class EditInstructionArgumentTriggerDirective { - @Input() instructionArgument?: Document; +@Directive({ selector: '[bdEditInstructionArgument]' }) +export class EditInstructionArgumentDirective { + @Input() instructionArgument?: InstructionArgumentDto; @Output() editInstructionArgument = new EventEmitter(); @HostListener('click') onClick(): void { this._matDialog .open< EditInstructionArgumentComponent, - { instructionArgument?: Document }, + InstructionArgumentDto, InstructionArgumentDto >(EditInstructionArgumentComponent, { - data: { instructionArgument: this.instructionArgument }, + data: this.instructionArgument, + panelClass: 'bd-bg-image-7', }) .afterClosed() .subscribe((data) => data && this.editInstructionArgument.emit(data)); diff --git a/libs/bulldozer/client/instructions/modals/edit-instruction-argument/src/lib/edit-instruction-argument.module.ts b/libs/bulldozer/client/instructions/modals/edit-instruction-argument/src/lib/edit-instruction-argument.module.ts index 76bf0891..b7e06edb 100644 --- a/libs/bulldozer/client/instructions/modals/edit-instruction-argument/src/lib/edit-instruction-argument.module.ts +++ b/libs/bulldozer/client/instructions/modals/edit-instruction-argument/src/lib/edit-instruction-argument.module.ts @@ -8,8 +8,8 @@ import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; -import { EditInstructionArgumentTriggerDirective } from './edit-instruction-argument-trigger.directive'; import { EditInstructionArgumentComponent } from './edit-instruction-argument.component'; +import { EditInstructionArgumentDirective } from './edit-instruction-argument.directive'; @NgModule({ imports: [ @@ -25,8 +25,8 @@ import { EditInstructionArgumentComponent } from './edit-instruction-argument.co ], declarations: [ EditInstructionArgumentComponent, - EditInstructionArgumentTriggerDirective, + EditInstructionArgumentDirective, ], - exports: [EditInstructionArgumentTriggerDirective], + exports: [EditInstructionArgumentDirective], }) export class EditInstructionArgumentModule {} diff --git a/libs/bulldozer/client/instructions/modals/edit-instruction-document/src/lib/edit-instruction-document.component.ts b/libs/bulldozer/client/instructions/modals/edit-instruction-document/src/lib/edit-instruction-document.component.ts index db35fad1..86106521 100644 --- a/libs/bulldozer/client/instructions/modals/edit-instruction-document/src/lib/edit-instruction-document.component.ts +++ b/libs/bulldozer/client/instructions/modals/edit-instruction-document/src/lib/edit-instruction-document.component.ts @@ -9,18 +9,17 @@ import { import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { InstructionAccountItemView } from '@bulldozer-client/instructions-data-access'; -import { - Collection, - Document, - InstructionAccount, -} from '@heavy-duty/bulldozer-devkit'; +import {} from '@bulldozer-client/instructions-data-access'; +import { SnackBarComponent } from '@bulldozer-client/notification-snack-bar'; +import { InstructionAccountDto } from '@heavy-duty/bulldozer-devkit'; +import { List } from 'immutable'; import { Subject, takeUntil } from 'rxjs'; +import { Collection, InstructionAccount } from './types'; @Component({ selector: 'bd-edit-document', template: ` -

+

{{ data?.document ? 'Edit' : 'Create' }} document

@@ -117,10 +116,10 @@ import { Subject, takeUntil } from 'rxjs'; - {{ account.document.name }} | - {{ account.document.id | obscureAddress }} + {{ account.name }} | + {{ account.id | obscureAddress }} The payer is required. @@ -134,35 +133,37 @@ import { Subject, takeUntil } from 'rxjs'; > Close - None + None - {{ account.document.name }} | - {{ account.document.id | obscureAddress }} + {{ account.name }} | + {{ account.id | obscureAddress }} - - + + - +
+
+
+
+
+
+ + `, changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -197,27 +198,22 @@ export class EditInstructionDocumentComponent implements OnInit, OnDestroy { private readonly _matDialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data?: { - document?: Document; - collections: Document[]; - accounts: InstructionAccountItemView[]; + document?: InstructionAccountDto; + collections: List; + accounts: List; } ) { this.form = new FormGroup({ name: new FormControl(this.data?.document?.name ?? '', { validators: [Validators.required], }), - modifier: new FormControl( - this.data?.document?.data.modifier !== null - ? this.data?.document?.data.modifier.id - : null - ), - collection: new FormControl( - this.data?.document?.data.kind.collection || null, - { validators: [Validators.required] } - ), - space: new FormControl(this.data?.document?.data.modifier?.space), - payer: new FormControl(this.data?.document?.data.modifier?.payer), - close: new FormControl(this.data?.document?.data.modifier?.close), + modifier: new FormControl(this.data?.document?.modifier ?? null), + collection: new FormControl(this.data?.document?.collection ?? null, { + validators: [Validators.required], + }), + space: new FormControl(this.data?.document?.space ?? null), + payer: new FormControl(this.data?.document?.payer ?? null), + close: new FormControl(this.data?.document?.close ?? null), }); } @@ -265,9 +261,13 @@ export class EditInstructionDocumentComponent implements OnInit, OnDestroy { this.modifierControl.value === 1 ? this.closeControl.value : null, }); } else { - this._matSnackBar.open('Invalid information', 'close', { - panelClass: 'warning-snackbar', + this._matSnackBar.openFromComponent(SnackBarComponent, { duration: 5000, + data: { + title: 'Heey...', + message: 'Invalid Information', + type: 'warning', + }, }); } } diff --git a/libs/bulldozer/client/instructions/modals/edit-instruction-document/src/lib/edit-instruction-document-trigger.directive.ts b/libs/bulldozer/client/instructions/modals/edit-instruction-document/src/lib/edit-instruction-document.directive.ts similarity index 57% rename from libs/bulldozer/client/instructions/modals/edit-instruction-document/src/lib/edit-instruction-document-trigger.directive.ts rename to libs/bulldozer/client/instructions/modals/edit-instruction-document/src/lib/edit-instruction-document.directive.ts index 74227fec..59b64a81 100644 --- a/libs/bulldozer/client/instructions/modals/edit-instruction-document/src/lib/edit-instruction-document-trigger.directive.ts +++ b/libs/bulldozer/client/instructions/modals/edit-instruction-document/src/lib/edit-instruction-document.directive.ts @@ -6,20 +6,16 @@ import { Output, } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { InstructionAccountItemView } from '@bulldozer-client/instructions-data-access'; -import { - Collection, - Document, - InstructionAccount, - InstructionAccountDto, -} from '@heavy-duty/bulldozer-devkit'; +import { InstructionAccountDto } from '@heavy-duty/bulldozer-devkit'; +import { List } from 'immutable'; import { EditInstructionDocumentComponent } from './edit-instruction-document.component'; +import { Collection, InstructionAccount } from './types'; -@Directive({ selector: '[bdEditInstructionDocumentTrigger]' }) -export class EditInstructionDocumentTriggerDirective { - @Input() instructionDocument: Document | null = null; - @Input() collections: Document[] | null = null; - @Input() instructionAccounts: InstructionAccountItemView[] | null = null; +@Directive({ selector: '[bdEditInstructionDocument]' }) +export class EditInstructionDocumentDirective { + @Input() instructionDocument: InstructionAccountDto | null = null; + @Input() collections: List | null = null; + @Input() instructionAccounts: List | null = null; @Output() editInstructionDocument = new EventEmitter(); @HostListener('click') onClick(): void { if (!this.collections || !this.instructionAccounts) { @@ -30,9 +26,9 @@ export class EditInstructionDocumentTriggerDirective { .open< EditInstructionDocumentComponent, { - document: Document | null; - collections: Document[]; - accounts: InstructionAccountItemView[]; + document: InstructionAccountDto | null; + collections: List; + accounts: List; }, InstructionAccountDto >(EditInstructionDocumentComponent, { @@ -41,6 +37,7 @@ export class EditInstructionDocumentTriggerDirective { collections: this.collections, accounts: this.instructionAccounts, }, + panelClass: 'bd-bg-image-7', }) .afterClosed() .subscribe((data) => data && this.editInstructionDocument.emit(data)); diff --git a/libs/bulldozer/client/instructions/modals/edit-instruction-document/src/lib/edit-instruction-document.module.ts b/libs/bulldozer/client/instructions/modals/edit-instruction-document/src/lib/edit-instruction-document.module.ts index ee97c9c9..a5516447 100644 --- a/libs/bulldozer/client/instructions/modals/edit-instruction-document/src/lib/edit-instruction-document.module.ts +++ b/libs/bulldozer/client/instructions/modals/edit-instruction-document/src/lib/edit-instruction-document.module.ts @@ -9,8 +9,8 @@ import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { ObscureAddressModule } from '@bulldozer-client/obscure-address'; -import { EditInstructionDocumentTriggerDirective } from './edit-instruction-document-trigger.directive'; import { EditInstructionDocumentComponent } from './edit-instruction-document.component'; +import { EditInstructionDocumentDirective } from './edit-instruction-document.directive'; @NgModule({ imports: [ @@ -27,8 +27,8 @@ import { EditInstructionDocumentComponent } from './edit-instruction-document.co ], declarations: [ EditInstructionDocumentComponent, - EditInstructionDocumentTriggerDirective, + EditInstructionDocumentDirective, ], - exports: [EditInstructionDocumentTriggerDirective], + exports: [EditInstructionDocumentDirective], }) export class EditInstructionDocumentModule {} diff --git a/libs/bulldozer/client/instructions/modals/edit-instruction-document/src/lib/types.ts b/libs/bulldozer/client/instructions/modals/edit-instruction-document/src/lib/types.ts new file mode 100644 index 00000000..f597f4f4 --- /dev/null +++ b/libs/bulldozer/client/instructions/modals/edit-instruction-document/src/lib/types.ts @@ -0,0 +1,9 @@ +export interface Collection { + id: string; + name: string; +} + +export interface InstructionAccount { + id: string; + name: string; +} diff --git a/libs/bulldozer/client/instructions/modals/edit-instruction-relation/src/lib/edit-instruction-relation.component.ts b/libs/bulldozer/client/instructions/modals/edit-instruction-relation/src/lib/edit-instruction-relation.component.ts index ad9dff01..43576c49 100644 --- a/libs/bulldozer/client/instructions/modals/edit-instruction-relation/src/lib/edit-instruction-relation.component.ts +++ b/libs/bulldozer/client/instructions/modals/edit-instruction-relation/src/lib/edit-instruction-relation.component.ts @@ -7,7 +7,9 @@ import { } from '@angular/forms'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { InstructionAccountItemView } from '@bulldozer-client/instructions-data-access'; +import { SnackBarComponent } from '@bulldozer-client/notification-snack-bar'; +import { List } from 'immutable'; +import { InstructionAccount } from './types'; export const equalValidator = (a: string, b: string): ValidatorFn => @@ -22,7 +24,7 @@ export const equalValidator = @Component({ selector: 'bd-edit-relation', template: ` -

Create relation

+

Create relation

- {{ account.document.name }} | - {{ account.document.id | obscureAddress }} + {{ account.name }} | + {{ account.id | obscureAddress }} To is required. @@ -53,24 +55,25 @@ export const equalValidator = >Accounts have to be different. - + + +
+
+
+
+
+
+
- - `, }) export class EditInstructionRelationComponent { @@ -83,7 +86,7 @@ export class EditInstructionRelationComponent { private readonly _matDialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: { - accounts: InstructionAccountItemView[]; + accounts: List; from: string; } ) { @@ -107,9 +110,13 @@ export class EditInstructionRelationComponent { if (this.form.valid) { this._matDialogRef.close(this.form.value); } else { - this._matSnackBar.open('Invalid information', 'close', { - panelClass: 'warning-snackbar', + this._matSnackBar.openFromComponent(SnackBarComponent, { duration: 5000, + data: { + title: 'Heey...', + message: 'Invalid Information', + type: 'warning', + }, }); } } diff --git a/libs/bulldozer/client/instructions/modals/edit-instruction-relation/src/lib/edit-instruction-relation-trigger.directive.ts b/libs/bulldozer/client/instructions/modals/edit-instruction-relation/src/lib/edit-instruction-relation.directive.ts similarity index 71% rename from libs/bulldozer/client/instructions/modals/edit-instruction-relation/src/lib/edit-instruction-relation-trigger.directive.ts rename to libs/bulldozer/client/instructions/modals/edit-instruction-relation/src/lib/edit-instruction-relation.directive.ts index 00e6d6fc..82135f60 100644 --- a/libs/bulldozer/client/instructions/modals/edit-instruction-relation/src/lib/edit-instruction-relation-trigger.directive.ts +++ b/libs/bulldozer/client/instructions/modals/edit-instruction-relation/src/lib/edit-instruction-relation.directive.ts @@ -6,13 +6,15 @@ import { Output, } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { InstructionAccountItemView } from '@bulldozer-client/instructions-data-access'; +import {} from '@bulldozer-client/instructions-data-access'; import { InstructionRelationDto } from '@heavy-duty/bulldozer-devkit'; +import { List } from 'immutable'; import { EditInstructionRelationComponent } from './edit-instruction-relation.component'; +import { InstructionAccount } from './types'; -@Directive({ selector: '[bdEditInstructionRelationTrigger]' }) -export class EditInstructionRelationTriggerDirective { - @Input() instructionAccounts: InstructionAccountItemView[] | null = null; +@Directive({ selector: '[bdEditInstructionRelation]' }) +export class EditInstructionRelationDirective { + @Input() instructionAccounts: List | null = null; @Input() from?: string; @Output() editInstructionRelation = new EventEmitter(); @@ -25,7 +27,7 @@ export class EditInstructionRelationTriggerDirective { .open< EditInstructionRelationComponent, { - accounts: InstructionAccountItemView[]; + accounts: List; from: string; }, InstructionRelationDto @@ -34,6 +36,7 @@ export class EditInstructionRelationTriggerDirective { accounts: this.instructionAccounts, from: this.from, }, + panelClass: 'bd-bg-image-7', }) .afterClosed() .subscribe((data) => data && this.editInstructionRelation.emit(data)); diff --git a/libs/bulldozer/client/instructions/modals/edit-instruction-relation/src/lib/edit-instruction-relation.module.ts b/libs/bulldozer/client/instructions/modals/edit-instruction-relation/src/lib/edit-instruction-relation.module.ts index dc64bd2c..768c2756 100644 --- a/libs/bulldozer/client/instructions/modals/edit-instruction-relation/src/lib/edit-instruction-relation.module.ts +++ b/libs/bulldozer/client/instructions/modals/edit-instruction-relation/src/lib/edit-instruction-relation.module.ts @@ -8,8 +8,8 @@ import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { ObscureAddressModule } from '@bulldozer-client/obscure-address'; -import { EditInstructionRelationTriggerDirective } from './edit-instruction-relation-trigger.directive'; import { EditInstructionRelationComponent } from './edit-instruction-relation.component'; +import { EditInstructionRelationDirective } from './edit-instruction-relation.directive'; @NgModule({ imports: [ @@ -25,8 +25,8 @@ import { EditInstructionRelationComponent } from './edit-instruction-relation.co ], declarations: [ EditInstructionRelationComponent, - EditInstructionRelationTriggerDirective, + EditInstructionRelationDirective, ], - exports: [EditInstructionRelationTriggerDirective], + exports: [EditInstructionRelationDirective], }) export class EditInstructionRelationModule {} diff --git a/libs/bulldozer/client/instructions/modals/edit-instruction-relation/src/lib/types.ts b/libs/bulldozer/client/instructions/modals/edit-instruction-relation/src/lib/types.ts new file mode 100644 index 00000000..f597f4f4 --- /dev/null +++ b/libs/bulldozer/client/instructions/modals/edit-instruction-relation/src/lib/types.ts @@ -0,0 +1,9 @@ +export interface Collection { + id: string; + name: string; +} + +export interface InstructionAccount { + id: string; + name: string; +} diff --git a/libs/bulldozer/client/instructions/modals/edit-instruction-signer/src/lib/edit-instruction-signer.component.ts b/libs/bulldozer/client/instructions/modals/edit-instruction-signer/src/lib/edit-instruction-signer.component.ts index 438c10c4..b423ac97 100644 --- a/libs/bulldozer/client/instructions/modals/edit-instruction-signer/src/lib/edit-instruction-signer.component.ts +++ b/libs/bulldozer/client/instructions/modals/edit-instruction-signer/src/lib/edit-instruction-signer.component.ts @@ -2,13 +2,14 @@ import { Component, HostBinding, Inject } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { Document, InstructionAccount } from '@heavy-duty/bulldozer-devkit'; +import { SnackBarComponent } from '@bulldozer-client/notification-snack-bar'; +import { InstructionAccountDto } from '@heavy-duty/bulldozer-devkit'; @Component({ selector: 'bd-edit-instruction-signer', template: ` -

- {{ data?.signer ? 'Edit' : 'Create' }} signer +

+ {{ signer ? 'Edit' : 'Create' }} signer

Save changes. - + + +
+
+
+
+
+
+
- - `, }) export class EditInstructionSignerComponent { @@ -77,15 +79,13 @@ export class EditInstructionSignerComponent { private readonly _matSnackBar: MatSnackBar, private readonly _matDialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) - public data?: { - signer?: Document; - } + public signer?: InstructionAccountDto ) { this.form = new FormGroup({ - name: new FormControl(this.data?.signer?.name ?? '', { + name: new FormControl(this.signer?.name ?? '', { validators: [Validators.required], }), - saveChanges: new FormControl(this.data?.signer?.data.modifier?.id === 1), + saveChanges: new FormControl(this.signer?.modifier === 1), }); } @@ -104,9 +104,13 @@ export class EditInstructionSignerComponent { close: null, }); } else { - this._matSnackBar.open('Invalid information', 'close', { - panelClass: 'warning-snackbar', + this._matSnackBar.openFromComponent(SnackBarComponent, { duration: 5000, + data: { + title: 'Heey...', + message: 'Invalid Information', + type: 'warning', + }, }); } } diff --git a/libs/bulldozer/client/instructions/modals/edit-instruction-signer/src/lib/edit-instruction-signer-trigger.directive.ts b/libs/bulldozer/client/instructions/modals/edit-instruction-signer/src/lib/edit-instruction-signer.directive.ts similarity index 61% rename from libs/bulldozer/client/instructions/modals/edit-instruction-signer/src/lib/edit-instruction-signer-trigger.directive.ts rename to libs/bulldozer/client/instructions/modals/edit-instruction-signer/src/lib/edit-instruction-signer.directive.ts index 0244a3da..5f1fd709 100644 --- a/libs/bulldozer/client/instructions/modals/edit-instruction-signer/src/lib/edit-instruction-signer-trigger.directive.ts +++ b/libs/bulldozer/client/instructions/modals/edit-instruction-signer/src/lib/edit-instruction-signer.directive.ts @@ -6,29 +6,22 @@ import { Output, } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { - Document, - InstructionAccount, - InstructionAccountDto, -} from '@heavy-duty/bulldozer-devkit'; +import { InstructionAccountDto } from '@heavy-duty/bulldozer-devkit'; import { EditInstructionSignerComponent } from './edit-instruction-signer.component'; -@Directive({ selector: '[bdEditInstructionSignerTrigger]' }) -export class EditInstructionSignerTriggerDirective { - @Input() instructionSigner?: Document; +@Directive({ selector: '[bdEditInstructionSigner]' }) +export class EditInstructionSignerDirective { + @Input() instructionSigner?: InstructionAccountDto; @Output() editInstructionSigner = new EventEmitter(); @HostListener('click') onClick(): void { this._matDialog .open< EditInstructionSignerComponent, - { - signer?: Document; - }, + InstructionAccountDto, InstructionAccountDto >(EditInstructionSignerComponent, { - data: { - signer: this.instructionSigner, - }, + data: this.instructionSigner, + panelClass: 'bd-bg-image-7', }) .afterClosed() .subscribe((data) => data && this.editInstructionSigner.emit(data)); diff --git a/libs/bulldozer/client/instructions/modals/edit-instruction-signer/src/lib/edit-instruction-signer.module.ts b/libs/bulldozer/client/instructions/modals/edit-instruction-signer/src/lib/edit-instruction-signer.module.ts index fbe2b22a..d0d231fa 100644 --- a/libs/bulldozer/client/instructions/modals/edit-instruction-signer/src/lib/edit-instruction-signer.module.ts +++ b/libs/bulldozer/client/instructions/modals/edit-instruction-signer/src/lib/edit-instruction-signer.module.ts @@ -7,8 +7,8 @@ import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; -import { EditInstructionSignerTriggerDirective } from './edit-instruction-signer-trigger.directive'; import { EditInstructionSignerComponent } from './edit-instruction-signer.component'; +import { EditInstructionSignerDirective } from './edit-instruction-signer.directive'; @NgModule({ imports: [ @@ -23,8 +23,8 @@ import { EditInstructionSignerComponent } from './edit-instruction-signer.compon ], declarations: [ EditInstructionSignerComponent, - EditInstructionSignerTriggerDirective, + EditInstructionSignerDirective, ], - exports: [EditInstructionSignerTriggerDirective], + exports: [EditInstructionSignerDirective], }) export class EditInstructionSignerModule {} diff --git a/libs/bulldozer/client/instructions/modals/edit-instruction/src/lib/edit-instruction.component.ts b/libs/bulldozer/client/instructions/modals/edit-instruction/src/lib/edit-instruction.component.ts index fb570f4c..a4e03793 100644 --- a/libs/bulldozer/client/instructions/modals/edit-instruction/src/lib/edit-instruction.component.ts +++ b/libs/bulldozer/client/instructions/modals/edit-instruction/src/lib/edit-instruction.component.ts @@ -2,13 +2,14 @@ import { Component, HostBinding, Inject } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { Document, Instruction } from '@heavy-duty/bulldozer-devkit'; +import { SnackBarComponent } from '@bulldozer-client/notification-snack-bar'; +import { InstructionDto } from '@heavy-duty/bulldozer-devkit'; @Component({ selector: 'bd-edit-instruction', template: ` -

- {{ data?.instruction ? 'Edit' : 'Create' }} instruction +

+ {{ instruction ? 'Edit' : 'Create' }} instruction

- + + +
+
+
+
+
+
+
- - `, }) export class EditInstructionComponent { @@ -72,12 +74,10 @@ export class EditInstructionComponent { private readonly _matSnackBar: MatSnackBar, private readonly _matDialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) - public data?: { - instruction?: Document; - } + public instruction?: InstructionDto ) { this.form = new FormGroup({ - name: new FormControl(this.data?.instruction?.name ?? '', { + name: new FormControl(this.instruction?.name ?? '', { validators: [Validators.required], }), }); @@ -90,9 +90,13 @@ export class EditInstructionComponent { if (this.form.valid) { this._matDialogRef.close(this.form.value); } else { - this._matSnackBar.open('Invalid information', 'close', { - panelClass: 'warning-snackbar', + this._matSnackBar.openFromComponent(SnackBarComponent, { duration: 5000, + data: { + title: 'Heey...', + message: 'Invalid Information', + type: 'warning', + }, }); } } diff --git a/libs/bulldozer/client/instructions/modals/edit-instruction/src/lib/edit-instruction-trigger.directive.ts b/libs/bulldozer/client/instructions/modals/edit-instruction/src/lib/edit-instruction.directive.ts similarity index 50% rename from libs/bulldozer/client/instructions/modals/edit-instruction/src/lib/edit-instruction-trigger.directive.ts rename to libs/bulldozer/client/instructions/modals/edit-instruction/src/lib/edit-instruction.directive.ts index 85001cc5..a9ec0405 100644 --- a/libs/bulldozer/client/instructions/modals/edit-instruction/src/lib/edit-instruction-trigger.directive.ts +++ b/libs/bulldozer/client/instructions/modals/edit-instruction/src/lib/edit-instruction.directive.ts @@ -6,22 +6,21 @@ import { Output, } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { Document, Instruction } from '@heavy-duty/bulldozer-devkit'; +import { InstructionDto } from '@heavy-duty/bulldozer-devkit'; import { EditInstructionComponent } from './edit-instruction.component'; -@Directive({ selector: '[bdEditInstructionTrigger]' }) -export class EditInstructionTriggerDirective { - @Input() instruction?: Document; - @Output() editInstruction = new EventEmitter(); +@Directive({ selector: '[bdEditInstruction]' }) +export class EditInstructionDirective { + @Input() instruction?: InstructionDto; + @Output() editInstruction = new EventEmitter(); @HostListener('click') onClick(): void { this._matDialog - .open< + .open( EditInstructionComponent, - { instruction?: Document }, - { name: string } - >(EditInstructionComponent, { data: { instruction: this.instruction } }) + { data: this.instruction, panelClass: 'bd-bg-image-7' } + ) .afterClosed() - .subscribe((data) => data && this.editInstruction.emit(data.name)); + .subscribe((data) => data && this.editInstruction.emit(data)); } constructor(private readonly _matDialog: MatDialog) {} diff --git a/libs/bulldozer/client/instructions/modals/edit-instruction/src/lib/edit-instruction.module.ts b/libs/bulldozer/client/instructions/modals/edit-instruction/src/lib/edit-instruction.module.ts index f1ef35c4..ca21a63a 100644 --- a/libs/bulldozer/client/instructions/modals/edit-instruction/src/lib/edit-instruction.module.ts +++ b/libs/bulldozer/client/instructions/modals/edit-instruction/src/lib/edit-instruction.module.ts @@ -6,8 +6,8 @@ import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; -import { EditInstructionTriggerDirective } from './edit-instruction-trigger.directive'; import { EditInstructionComponent } from './edit-instruction.component'; +import { EditInstructionDirective } from './edit-instruction.directive'; @NgModule({ imports: [ @@ -19,7 +19,7 @@ import { EditInstructionComponent } from './edit-instruction.component'; MatIconModule, MatInputModule, ], - declarations: [EditInstructionComponent, EditInstructionTriggerDirective], - exports: [EditInstructionTriggerDirective], + declarations: [EditInstructionComponent, EditInstructionDirective], + exports: [EditInstructionDirective], }) export class EditInstructionModule {} diff --git a/libs/bulldozer/client/instructions/components/instruction-arguments-list/.eslintrc.json b/libs/bulldozer/client/instructions/pages/view-instruction-arguments/.eslintrc.json similarity index 100% rename from libs/bulldozer/client/instructions/components/instruction-arguments-list/.eslintrc.json rename to libs/bulldozer/client/instructions/pages/view-instruction-arguments/.eslintrc.json diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-arguments/README.md b/libs/bulldozer/client/instructions/pages/view-instruction-arguments/README.md new file mode 100644 index 00000000..965f351b --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-arguments/README.md @@ -0,0 +1,7 @@ +# bulldozer-client-instructions-pages-view-instruction-arguments + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test bulldozer-client-instructions-pages-view-instruction-arguments` to execute the unit tests. diff --git a/libs/bulldozer/client/instructions/components/instruction-documents-list/jest.config.js b/libs/bulldozer/client/instructions/pages/view-instruction-arguments/jest.config.js similarity index 83% rename from libs/bulldozer/client/instructions/components/instruction-documents-list/jest.config.js rename to libs/bulldozer/client/instructions/pages/view-instruction-arguments/jest.config.js index fd2a6e19..10651d56 100644 --- a/libs/bulldozer/client/instructions/components/instruction-documents-list/jest.config.js +++ b/libs/bulldozer/client/instructions/pages/view-instruction-arguments/jest.config.js @@ -1,6 +1,5 @@ module.exports = { - displayName: - 'bulldozer-client-instructions-components-instruction-documents-list', + displayName: 'bulldozer-client-instructions-pages-view-instruction-arguments', preset: '../../../../../../jest.preset.js', setupFilesAfterEnv: ['/src/test-setup.ts'], globals: { @@ -10,7 +9,7 @@ module.exports = { }, }, coverageDirectory: - '../../../../../../coverage/libs/bulldozer/client/instructions/components/instruction-documents-list', + '../../../../../../coverage/libs/bulldozer/client/instructions/pages/view-instruction-arguments', transform: { '^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular', }, diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-arguments/project.json b/libs/bulldozer/client/instructions/pages/view-instruction-arguments/project.json new file mode 100644 index 00000000..fae53451 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-arguments/project.json @@ -0,0 +1,28 @@ +{ + "projectType": "library", + "root": "libs/bulldozer/client/instructions/pages/view-instruction-arguments", + "sourceRoot": "libs/bulldozer/client/instructions/pages/view-instruction-arguments/src", + "prefix": "bd", + "targets": { + "test": { + "executor": "@nrwl/jest:jest", + "outputs": [ + "coverage/libs/bulldozer/client/instructions/pages/view-instruction-arguments" + ], + "options": { + "jestConfig": "libs/bulldozer/client/instructions/pages/view-instruction-arguments/jest.config.js", + "passWithNoTests": true + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "libs/bulldozer/client/instructions/pages/view-instruction-arguments/src/**/*.ts", + "libs/bulldozer/client/instructions/pages/view-instruction-arguments/src/**/*.html" + ] + } + } + }, + "tags": [] +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-arguments/src/index.ts b/libs/bulldozer/client/instructions/pages/view-instruction-arguments/src/index.ts new file mode 100644 index 00000000..fe50261c --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-arguments/src/index.ts @@ -0,0 +1 @@ +export * from './lib/view-instruction-arguments.module'; diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-arguments/src/lib/reduce-instructions.ts b/libs/bulldozer/client/instructions/pages/view-instruction-arguments/src/lib/reduce-instructions.ts new file mode 100644 index 00000000..74b89d5f --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-arguments/src/lib/reduce-instructions.ts @@ -0,0 +1,293 @@ +import { + InstructionArgumentDto, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { List } from 'immutable'; +import { InstructionArgumentItemView } from './types'; + +const decodeAttributeKind = ( + id: number, + size: number +): { id: number; name: string; size: number } => { + switch (id) { + case 0: + return { + id, + name: 'boolean', + size, + }; + case 1: + return { + id, + name: 'number', + size, + }; + case 2: + return { + id, + name: 'string', + size, + }; + case 3: + return { + id, + name: 'pubkey', + size, + }; + default: + throw Error('Invalid kind id'); + } +}; + +const decodeAttributeModifier = ( + id: number, + size: number +): { id: number; name: string; size: number } => { + switch (id) { + case 0: + return { + id, + name: 'array', + size, + }; + case 1: + return { + id, + name: 'vector', + size, + }; + default: + throw Error('Invalid kind id'); + } +}; + +const getAttributeKindSize = ( + kind: number, + extensions: { max: number | null; maxLength: number | null } +): number => { + switch (kind) { + case 0: + return 1; + case 1: + if (extensions.max === null) { + throw Error('Max is required for numbers'); + } + return extensions.max; + case 2: + if (extensions.maxLength === null) { + throw Error('Max length is required for strings'); + } + return extensions.maxLength; + case 3: + return 32; + default: + throw Error('Invalid Attribute Kind'); + } +}; + +export const reduceInstructions = ( + items: List, + instruction: InstructionStatus +): List => { + switch (instruction.name) { + case 'createInstructionArgument': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: InstructionArgumentDto; + }; + const name = data.arguments.name; + const kind = decodeAttributeKind( + data.arguments.kind, + getAttributeKindSize(data.arguments.kind, { + max: data.arguments.max, + maxLength: data.arguments.maxLength, + }) + ); + let modifier: { + id: number; + name: string; + size: number; + } | null = null; + if (data.arguments.modifier !== null && data.arguments.size !== null) { + modifier = decodeAttributeModifier( + data.arguments.modifier, + data.arguments.size + ); + } + + const workspaceId = instruction.accounts.find( + (account) => account.name === 'Workspace' + )?.pubkey; + const applicationId = instruction.accounts.find( + (account) => account.name === 'Application' + )?.pubkey; + const instructionId = instruction.accounts.find( + (account) => account.name === 'Instruction' + )?.pubkey; + const argumentId = instruction.accounts.find( + (account) => account.name === 'Argument' + )?.pubkey; + + if ( + workspaceId === undefined || + applicationId === undefined || + instructionId === undefined || + argumentId === undefined + ) { + throw new Error('Malformed Create Instruction Argument'); + } + + const itemIndex = items.findIndex((item) => item.id === argumentId); + + if (itemIndex === -1) { + return items.push({ + id: argumentId, + name, + kind, + modifier, + isCreating: true, + isUpdating: false, + isDeleting: false, + instructionId, + applicationId, + workspaceId, + }); + } else { + return items.update(itemIndex, (item) => ({ + ...item, + id: argumentId, + name, + kind, + modifier, + isCreating: true, + isUpdating: false, + isDeleting: false, + instructionId, + applicationId, + workspaceId, + })); + } + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Argument' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isCreating: false, + }; + }); + } else { + return items; + } + } + case 'updateInstructionArgument': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: InstructionArgumentDto; + }; + const name = data.arguments.name; + const kind = decodeAttributeKind( + data.arguments.kind, + getAttributeKindSize(data.arguments.kind, { + max: data.arguments.max, + maxLength: data.arguments.maxLength, + }) + ); + let modifier: { + id: number; + name: string; + size: number; + } | null = null; + if (data.arguments.modifier !== null && data.arguments.size !== null) { + modifier = decodeAttributeModifier( + data.arguments.modifier, + data.arguments.size + ); + } + + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Argument' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + name, + kind, + modifier, + isUpdating: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Argument' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isUpdating: false, + }; + }); + } else { + return items; + } + } + case 'deleteInstructionArgument': + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Argument' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isDeleting: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.filter( + (item) => + !instruction.accounts.some( + (account) => + account.name === 'Argument' && account.pubkey === item.id + ) + ); + } else { + return items; + } + default: + return items; + } +}; diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-arguments/src/lib/types.ts b/libs/bulldozer/client/instructions/pages/view-instruction-arguments/src/lib/types.ts new file mode 100644 index 00000000..443bb937 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-arguments/src/lib/types.ts @@ -0,0 +1,20 @@ +export interface InstructionArgumentItemView { + id: string; + name: string; + kind: { + id: number; + name: string; + size: number; + }; + modifier: { + id: number; + name: string; + size: number; + } | null; + isCreating: boolean; + isUpdating: boolean; + isDeleting: boolean; + instructionId: string; + applicationId: string; + workspaceId: string; +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-arguments/src/lib/view-instruction-arguments.component.ts b/libs/bulldozer/client/instructions/pages/view-instruction-arguments/src/lib/view-instruction-arguments.component.ts new file mode 100644 index 00000000..a9730e98 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-arguments/src/lib/view-instruction-arguments.component.ts @@ -0,0 +1,429 @@ +import { + ChangeDetectionStrategy, + Component, + HostBinding, + OnInit, +} from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { + InstructionArgumentApiService, + InstructionArgumentsStore, +} from '@bulldozer-client/instructions-data-access'; +import { NotificationStore } from '@bulldozer-client/notifications-data-access'; +import { HdBroadcasterSocketStore } from '@heavy-duty/broadcaster'; +import { InstructionArgumentDto } from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { distinctUntilChanged, map } from 'rxjs'; +import { InstructionArgumentItemView } from './types'; +import { ViewInstructionArgumentsStore } from './view-instruction-arguments.store'; + +@Component({ + selector: 'bd-view-instruction-arguments', + template: ` +
+
+

Arguments

+

+ The arguments are the input parameters of your instruction. +

+ + + + + + + + + +
+
+ +
+ + + + + +
+
+ +
+ +
+ +
+ + +

+ + Creating + + + Updating + + + Deleting + +

+
+ +
+
+

+ {{ instructionArgument.name }} +

+

+ {{ instructionArgument.kind.name }}. +

+
+
+

+ auto_awesome_motion +   + + + Array of Items + + + Vector of Items + + + + + Single Item + +

+

+ data_array +   Size: + {{ instructionArgument.modifier?.size }} +

+
+
+
+ + + + +
+
+
+
+
+
+
+ + +
+

+ You're + offline. +

+
+
+ + +
+

+ You're currently + disconnected. +

+
+
+ + +
+ +

Loading data...

+
+
+ + +

There's no arguments yet.

+
+
+ `, + styles: [], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [InstructionArgumentsStore, ViewInstructionArgumentsStore], +}) +export class ViewInstructionArgumentsComponent implements OnInit { + @HostBinding('class') class = 'block p-8 pt-5 h-full'; + readonly workspaceId$ = this._route.paramMap.pipe( + map((paramMap) => paramMap.get('workspaceId')), + isNotNullOrUndefined, + distinctUntilChanged() + ); + readonly applicationId$ = this._route.paramMap.pipe( + map((paramMap) => paramMap.get('applicationId')), + isNotNullOrUndefined, + distinctUntilChanged() + ); + readonly instructionId$ = this._route.paramMap.pipe( + map((paramMap) => paramMap.get('instructionId')), + isNotNullOrUndefined, + distinctUntilChanged() + ); + readonly loading$ = this._instructionArgumentsStore.loading$; + readonly instructionArguments$ = + this._viewInstructionArgumentsStore.instructionArguments$; + + constructor( + private readonly _route: ActivatedRoute, + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _notificationStore: NotificationStore, + private readonly _instructionArgumentApiService: InstructionArgumentApiService, + private readonly _instructionArgumentsStore: InstructionArgumentsStore, + private readonly _viewInstructionArgumentsStore: ViewInstructionArgumentsStore + ) {} + + ngOnInit() { + this._viewInstructionArgumentsStore.setInstructionId(this.instructionId$); + } + + onCreateInstructionArgument( + authority: string, + workspaceId: string, + applicationId: string, + instructionId: string, + instructionArgumentDto: InstructionArgumentDto + ) { + this._instructionArgumentApiService + .create({ + instructionArgumentDto, + authority, + workspaceId, + applicationId, + instructionId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Create argument request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `instructionArguments:${instructionId}`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); + } + + onUpdateInstructionArgument( + authority: string, + workspaceId: string, + instructionId: string, + instructionArgumentId: string, + instructionArgumentDto: InstructionArgumentDto + ) { + this._instructionArgumentApiService + .update({ + authority, + workspaceId, + instructionId, + instructionArgumentDto, + instructionArgumentId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Update argument request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `instructionArguments:${instructionId}`, + ], + }, + }) + ); + }, + error: (error) => this._notificationStore.setError(error), + }); + } + + onDeleteInstructionArgument( + authority: string, + workspaceId: string, + instructionId: string, + instructionArgumentId: string + ) { + this._instructionArgumentApiService + .delete({ + authority, + workspaceId, + instructionArgumentId, + instructionId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Delete argument request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `instructionArguments:${instructionId}`, + ], + }, + }) + ); + }, + error: (error) => this._notificationStore.setError(error), + }); + } + + identify(_: number, instructionArgument: InstructionArgumentItemView) { + return instructionArgument.id; + } +} diff --git a/libs/bulldozer/client/instructions/components/instruction-arguments-list/src/lib/instruction-arguments-list.module.ts b/libs/bulldozer/client/instructions/pages/view-instruction-arguments/src/lib/view-instruction-arguments.module.ts similarity index 60% rename from libs/bulldozer/client/instructions/components/instruction-arguments-list/src/lib/instruction-arguments-list.module.ts rename to libs/bulldozer/client/instructions/pages/view-instruction-arguments/src/lib/view-instruction-arguments.module.ts index d95d92c2..57619c46 100644 --- a/libs/bulldozer/client/instructions/components/instruction-arguments-list/src/lib/instruction-arguments-list.module.ts +++ b/libs/bulldozer/client/instructions/pages/view-instruction-arguments/src/lib/view-instruction-arguments.module.ts @@ -7,16 +7,25 @@ import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { RouterModule } from '@angular/router'; +import { CardModule } from '@bulldozer-client/bd-card'; import { EditInstructionArgumentModule } from '@bulldozer-client/edit-instruction-argument'; import { ItemUpdatingModule } from '@bulldozer-client/item-updating'; -import { SectionHeaderModule } from '@bulldozer-client/section-header'; +import { HdBroadcasterCdkModule } from '@heavy-duty/broadcaster-cdk'; +import { HdWalletAdapterCdkModule } from '@heavy-duty/wallet-adapter-cdk'; import { ReactiveComponentModule } from '@ngrx/component'; -import { InstructionArgumentsListComponent } from './instruction-arguments-list.component'; +import { ViewInstructionArgumentsComponent } from './view-instruction-arguments.component'; @NgModule({ - declarations: [InstructionArgumentsListComponent], imports: [ CommonModule, + RouterModule.forChild([ + { + path: '', + pathMatch: 'full', + component: ViewInstructionArgumentsComponent, + }, + ]), MatButtonModule, MatCardModule, MatIconModule, @@ -25,10 +34,12 @@ import { InstructionArgumentsListComponent } from './instruction-arguments-list. MatProgressSpinnerModule, MatTooltipModule, ReactiveComponentModule, - SectionHeaderModule, - EditInstructionArgumentModule, + HdWalletAdapterCdkModule, + HdBroadcasterCdkModule, ItemUpdatingModule, + EditInstructionArgumentModule, + CardModule, ], - exports: [InstructionArgumentsListComponent], + declarations: [ViewInstructionArgumentsComponent], }) -export class InstructionArgumentsListModule {} +export class ViewInstructionArgumentsModule {} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-arguments/src/lib/view-instruction-arguments.store.ts b/libs/bulldozer/client/instructions/pages/view-instruction-arguments/src/lib/view-instruction-arguments.store.ts new file mode 100644 index 00000000..fbeb37c4 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-arguments/src/lib/view-instruction-arguments.store.ts @@ -0,0 +1,201 @@ +import { Injectable } from '@angular/core'; +import { InstructionArgumentsStore } from '@bulldozer-client/instructions-data-access'; +import { + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; +import { + Document, + flattenInstructions, + InstructionArgument, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined, isTruthy } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-instructions'; +import { InstructionArgumentItemView } from './types'; + +const documentToView = ( + document: Document +): InstructionArgumentItemView => { + return { + id: document.id, + name: document.name, + isCreating: false, + isUpdating: false, + isDeleting: false, + kind: document.data.kind, + modifier: document.data.modifier, + instructionId: document.data.instruction, + applicationId: document.data.application, + workspaceId: document.data.workspace, + }; +}; + +interface ViewModel { + instructionId: string | null; + transactions: List; +} + +const initialState: ViewModel = { + instructionId: null, + transactions: List(), +}; + +@Injectable() +export class ViewInstructionArgumentsStore extends ComponentStore { + private readonly _instructionId$ = this.select( + ({ instructionId }) => instructionId + ); + private readonly _topicName$ = this.select( + this._instructionId$.pipe(isNotNullOrUndefined), + (instructionId) => `instructionArguments:${instructionId}` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly instructionArguments$ = this.select( + this._instructionArgumentsStore.instructionArguments$, + this._instructionStatuses$, + (instructionsArguments, instructionStatuses) => { + if (instructionsArguments === null) { + return null; + } + + return instructionStatuses.reduce( + reduceInstructions, + instructionsArguments.map(documentToView) + ); + }, + { debounce: true } + ); + + constructor( + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _instructionArgumentsStore: InstructionArgumentsStore + ) { + super(initialState); + + this._instructionArgumentsStore.setFilters( + this.select( + this._instructionId$.pipe(isNotNullOrUndefined), + this._hdBroadcasterSocketStore.connected$.pipe(isTruthy), + (instructionId) => ({ instruction: instructionId }) + ) + ); + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) + ) + ); + } + + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) + ); + + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) + ); + + readonly setInstructionId = this.updater( + (state, instructionId) => ({ + ...state, + instructionId, + }) + ); + + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); + } + }) + ); + + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; + }>( + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } + + this.patchState({ transactions: List() }); + + const correlationId = uuid(); + let subscriptionId: string; + + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } + + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName + ); + } + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } + }) + ); + }) + ); +} diff --git a/libs/bulldozer/client/instructions/components/instruction-documents-list/src/test-setup.ts b/libs/bulldozer/client/instructions/pages/view-instruction-arguments/src/test-setup.ts similarity index 100% rename from libs/bulldozer/client/instructions/components/instruction-documents-list/src/test-setup.ts rename to libs/bulldozer/client/instructions/pages/view-instruction-arguments/src/test-setup.ts diff --git a/libs/bulldozer/client/instructions/components/instruction-arguments-list/tsconfig.json b/libs/bulldozer/client/instructions/pages/view-instruction-arguments/tsconfig.json similarity index 100% rename from libs/bulldozer/client/instructions/components/instruction-arguments-list/tsconfig.json rename to libs/bulldozer/client/instructions/pages/view-instruction-arguments/tsconfig.json diff --git a/libs/bulldozer/client/instructions/components/instruction-arguments-list/tsconfig.lib.json b/libs/bulldozer/client/instructions/pages/view-instruction-arguments/tsconfig.lib.json similarity index 100% rename from libs/bulldozer/client/instructions/components/instruction-arguments-list/tsconfig.lib.json rename to libs/bulldozer/client/instructions/pages/view-instruction-arguments/tsconfig.lib.json diff --git a/libs/bulldozer/client/instructions/components/instruction-arguments-list/tsconfig.spec.json b/libs/bulldozer/client/instructions/pages/view-instruction-arguments/tsconfig.spec.json similarity index 100% rename from libs/bulldozer/client/instructions/components/instruction-arguments-list/tsconfig.spec.json rename to libs/bulldozer/client/instructions/pages/view-instruction-arguments/tsconfig.spec.json diff --git a/libs/bulldozer/client/instructions/components/instruction-documents-list/.eslintrc.json b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/.eslintrc.json similarity index 100% rename from libs/bulldozer/client/instructions/components/instruction-documents-list/.eslintrc.json rename to libs/bulldozer/client/instructions/pages/view-instruction-code-editor/.eslintrc.json diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/README.md b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/README.md new file mode 100644 index 00000000..de0782bb --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/README.md @@ -0,0 +1,7 @@ +# bulldozer-client-instructions-pages-view-instruction-code-editor + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test bulldozer-client-instructions-pages-view-instruction-code-editor` to execute the unit tests. diff --git a/libs/bulldozer/client/instructions/components/instruction-signers-list/jest.config.js b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/jest.config.js similarity index 86% rename from libs/bulldozer/client/instructions/components/instruction-signers-list/jest.config.js rename to libs/bulldozer/client/instructions/pages/view-instruction-code-editor/jest.config.js index ef1da18c..ee59b08d 100644 --- a/libs/bulldozer/client/instructions/components/instruction-signers-list/jest.config.js +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/jest.config.js @@ -1,6 +1,6 @@ module.exports = { displayName: - 'bulldozer-client-instructions-components-instruction-signers-list', + 'bulldozer-client-instructions-pages-view-instruction-code-editor', preset: '../../../../../../jest.preset.js', setupFilesAfterEnv: ['/src/test-setup.ts'], globals: { @@ -10,7 +10,7 @@ module.exports = { }, }, coverageDirectory: - '../../../../../../coverage/libs/bulldozer/client/instructions/components/instruction-signers-list', + '../../../../../../coverage/libs/bulldozer/client/instructions/pages/view-instruction-code-editor', transform: { '^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular', }, diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/project.json b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/project.json new file mode 100644 index 00000000..d399e542 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/project.json @@ -0,0 +1,28 @@ +{ + "projectType": "library", + "root": "libs/bulldozer/client/instructions/pages/view-instruction-code-editor", + "sourceRoot": "libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src", + "prefix": "bd", + "targets": { + "test": { + "executor": "@nrwl/jest:jest", + "outputs": [ + "coverage/libs/bulldozer/client/instructions/pages/view-instruction-code-editor" + ], + "options": { + "jestConfig": "libs/bulldozer/client/instructions/pages/view-instruction-code-editor/jest.config.js", + "passWithNoTests": true + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/**/*.ts", + "libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/**/*.html" + ] + } + } + }, + "tags": [] +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/index.ts b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/index.ts new file mode 100644 index 00000000..d14d2997 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/index.ts @@ -0,0 +1 @@ +export * from './lib/view-instruction-code-editor.module'; diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-account-close-instructions.ts b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-account-close-instructions.ts new file mode 100644 index 00000000..7f3cb7f7 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-account-close-instructions.ts @@ -0,0 +1,68 @@ +import { InstructionStatus } from '@heavy-duty/bulldozer-devkit'; +import { List } from 'immutable'; +import { InstructionAccountCloseItemView } from './types'; + +export const reduceInstructions = ( + items: List, + instruction: InstructionStatus +): List => { + switch (instruction.name) { + case 'setInstructionAccountClose': { + const accountClose = instruction.accounts.find( + (account) => account.name === 'Account Close' + )?.pubkey; + const close = instruction.accounts.find( + (account) => account.name === 'Close' + )?.pubkey; + + if (close === undefined || accountClose === undefined) { + throw new Error('Malformed Set Instruction Account Close'); + } + + const itemIndex = items.findIndex((item) => item.id === accountClose); + + if (itemIndex === -1) { + return items.push({ + id: accountClose, + isUpdating: false, + close, + }); + } else { + return items.update(itemIndex, (item) => ({ + ...item, + id: accountClose, + isUpdating: false, + close, + })); + } + } + case 'clearInstructionAccountClose': { + const accountClose = instruction.accounts.find( + (account) => account.name === 'Account Close' + )?.pubkey; + + if (accountClose === undefined) { + throw new Error('Malformed Clear Instruction Account Close'); + } + + const itemIndex = items.findIndex((item) => item.id === accountClose); + + if (itemIndex === -1) { + return items.push({ + id: accountClose, + isUpdating: false, + close: null, + }); + } else { + return items.update(itemIndex, (item) => ({ + ...item, + id: accountClose, + isUpdating: false, + close: null, + })); + } + } + default: + return items; + } +}; diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-account-collection-instructions.ts b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-account-collection-instructions.ts new file mode 100644 index 00000000..1656e7ca --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-account-collection-instructions.ts @@ -0,0 +1,44 @@ +import { InstructionStatus } from '@heavy-duty/bulldozer-devkit'; +import { List } from 'immutable'; +import { InstructionAccountCollectionItemView } from './types'; + +export const reduceInstructions = ( + items: List, + instruction: InstructionStatus +): List => { + switch (instruction.name) { + case 'setInstructionAccountCollection': { + const accountCollection = instruction.accounts.find( + (account) => account.name === 'Account Collection' + )?.pubkey; + const collection = instruction.accounts.find( + (account) => account.name === 'Collection' + )?.pubkey; + + if (collection === undefined || accountCollection === undefined) { + throw new Error('Malformed Set Instruction Account Collection'); + } + + const itemIndex = items.findIndex( + (item) => item.id === accountCollection + ); + + if (itemIndex === -1) { + return items.push({ + id: accountCollection, + isUpdating: false, + collection, + }); + } else { + return items.update(itemIndex, (item) => ({ + ...item, + id: accountCollection, + isUpdating: false, + collection, + })); + } + } + default: + return items; + } +}; diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-account-instructions.ts b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-account-instructions.ts new file mode 100644 index 00000000..9cefa88a --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-account-instructions.ts @@ -0,0 +1,253 @@ +import { + InstructionAccountDto, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { List } from 'immutable'; +import { InstructionAccountItemView } from './types'; + +const decodeAccountKind = (id: number): { id: number; name: string } => { + switch (id) { + case 0: { + return { + id, + name: 'document', + }; + } + case 1: + return { + id, + name: 'signer', + }; + default: + throw Error('Invalid kind id'); + } +}; + +const decodeAccountModifier = ( + id: number +): { + id: number; + name: string; +} => { + switch (id) { + case 0: { + return { + id, + name: 'init', + }; + } + case 1: + return { + id, + name: 'mut', + }; + default: + throw Error('Invalid kind id'); + } +}; + +export const reduceInstructions = ( + items: List, + instruction: InstructionStatus +): List => { + switch (instruction.name) { + case 'createInstructionAccount': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: InstructionAccountDto; + }; + const name = data.arguments.name; + const space = data.arguments.space; + const kind = decodeAccountKind(data.arguments.kind); + let modifier: { + id: number; + name: string; + } | null = null; + if (data.arguments.modifier !== null) { + modifier = decodeAccountModifier(data.arguments.modifier); + } + + const workspaceId = instruction.accounts.find( + (account) => account.name === 'Workspace' + )?.pubkey; + const applicationId = instruction.accounts.find( + (account) => account.name === 'Application' + )?.pubkey; + const instructionId = instruction.accounts.find( + (account) => account.name === 'Instruction' + )?.pubkey; + const accountId = instruction.accounts.find( + (account) => account.name === 'Account' + )?.pubkey; + const collection = + instruction.accounts.find( + (account) => account.name === 'Account Collection' + )?.pubkey ?? null; + const close = + instruction.accounts.find( + (account) => account.name === 'Account Close' + )?.pubkey ?? null; + const payer = + instruction.accounts.find( + (account) => account.name === 'Account Payer' + )?.pubkey ?? null; + + if ( + workspaceId === undefined || + applicationId === undefined || + instructionId === undefined || + accountId === undefined + ) { + throw new Error('Malformed Create Instruction Account'); + } + + const itemIndex = items.findIndex((item) => item.id === accountId); + + if (itemIndex === -1) { + return items.push({ + id: accountId, + name, + kind, + modifier, + isCreating: true, + isUpdating: false, + isDeleting: false, + instructionId, + applicationId, + workspaceId, + space, + collection, + close, + payer, + }); + } else { + return items.update(itemIndex, (item) => ({ + ...item, + id: accountId, + name, + kind, + modifier, + isCreating: true, + isUpdating: false, + isDeleting: false, + instructionId, + applicationId, + workspaceId, + space, + collection, + close, + payer, + })); + } + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Account' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isCreating: false, + }; + }); + } else { + return items; + } + } + case 'updateInstructionAccount': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: InstructionAccountDto; + }; + const name = data.arguments.name; + let modifier: { + id: number; + name: string; + } | null = null; + if (data.arguments.modifier !== null) { + modifier = decodeAccountModifier(data.arguments.modifier); + } + + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Account' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + name, + modifier, + isUpdating: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Account' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isUpdating: false, + }; + }); + } else { + return items; + } + } + case 'deleteInstructionAccount': + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Account' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isDeleting: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.filter( + (item) => + !instruction.accounts.some( + (account) => + account.name === 'Account' && account.pubkey === item.id + ) + ); + } else { + return items; + } + default: + return items; + } +}; diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-account-payer-instructions.ts b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-account-payer-instructions.ts new file mode 100644 index 00000000..c7632a5c --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-account-payer-instructions.ts @@ -0,0 +1,42 @@ +import { InstructionStatus } from '@heavy-duty/bulldozer-devkit'; +import { List } from 'immutable'; +import { InstructionAccountPayerItemView } from './types'; + +export const reduceInstructions = ( + items: List, + instruction: InstructionStatus +): List => { + switch (instruction.name) { + case 'setInstructionAccountPayer': { + const accountPayer = instruction.accounts.find( + (account) => account.name === 'Account Payer' + )?.pubkey; + const payer = instruction.accounts.find( + (account) => account.name === 'Payer' + )?.pubkey; + + if (payer === undefined || accountPayer === undefined) { + throw new Error('Malformed Set Instruction Account Payer'); + } + + const itemIndex = items.findIndex((item) => item.id === accountPayer); + + if (itemIndex === -1) { + return items.push({ + id: accountPayer, + isUpdating: false, + payer, + }); + } else { + return items.update(itemIndex, (item) => ({ + ...item, + id: accountPayer, + isUpdating: false, + payer, + })); + } + } + default: + return items; + } +}; diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-argument-instructions.ts b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-argument-instructions.ts new file mode 100644 index 00000000..74b89d5f --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-argument-instructions.ts @@ -0,0 +1,293 @@ +import { + InstructionArgumentDto, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { List } from 'immutable'; +import { InstructionArgumentItemView } from './types'; + +const decodeAttributeKind = ( + id: number, + size: number +): { id: number; name: string; size: number } => { + switch (id) { + case 0: + return { + id, + name: 'boolean', + size, + }; + case 1: + return { + id, + name: 'number', + size, + }; + case 2: + return { + id, + name: 'string', + size, + }; + case 3: + return { + id, + name: 'pubkey', + size, + }; + default: + throw Error('Invalid kind id'); + } +}; + +const decodeAttributeModifier = ( + id: number, + size: number +): { id: number; name: string; size: number } => { + switch (id) { + case 0: + return { + id, + name: 'array', + size, + }; + case 1: + return { + id, + name: 'vector', + size, + }; + default: + throw Error('Invalid kind id'); + } +}; + +const getAttributeKindSize = ( + kind: number, + extensions: { max: number | null; maxLength: number | null } +): number => { + switch (kind) { + case 0: + return 1; + case 1: + if (extensions.max === null) { + throw Error('Max is required for numbers'); + } + return extensions.max; + case 2: + if (extensions.maxLength === null) { + throw Error('Max length is required for strings'); + } + return extensions.maxLength; + case 3: + return 32; + default: + throw Error('Invalid Attribute Kind'); + } +}; + +export const reduceInstructions = ( + items: List, + instruction: InstructionStatus +): List => { + switch (instruction.name) { + case 'createInstructionArgument': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: InstructionArgumentDto; + }; + const name = data.arguments.name; + const kind = decodeAttributeKind( + data.arguments.kind, + getAttributeKindSize(data.arguments.kind, { + max: data.arguments.max, + maxLength: data.arguments.maxLength, + }) + ); + let modifier: { + id: number; + name: string; + size: number; + } | null = null; + if (data.arguments.modifier !== null && data.arguments.size !== null) { + modifier = decodeAttributeModifier( + data.arguments.modifier, + data.arguments.size + ); + } + + const workspaceId = instruction.accounts.find( + (account) => account.name === 'Workspace' + )?.pubkey; + const applicationId = instruction.accounts.find( + (account) => account.name === 'Application' + )?.pubkey; + const instructionId = instruction.accounts.find( + (account) => account.name === 'Instruction' + )?.pubkey; + const argumentId = instruction.accounts.find( + (account) => account.name === 'Argument' + )?.pubkey; + + if ( + workspaceId === undefined || + applicationId === undefined || + instructionId === undefined || + argumentId === undefined + ) { + throw new Error('Malformed Create Instruction Argument'); + } + + const itemIndex = items.findIndex((item) => item.id === argumentId); + + if (itemIndex === -1) { + return items.push({ + id: argumentId, + name, + kind, + modifier, + isCreating: true, + isUpdating: false, + isDeleting: false, + instructionId, + applicationId, + workspaceId, + }); + } else { + return items.update(itemIndex, (item) => ({ + ...item, + id: argumentId, + name, + kind, + modifier, + isCreating: true, + isUpdating: false, + isDeleting: false, + instructionId, + applicationId, + workspaceId, + })); + } + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Argument' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isCreating: false, + }; + }); + } else { + return items; + } + } + case 'updateInstructionArgument': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: InstructionArgumentDto; + }; + const name = data.arguments.name; + const kind = decodeAttributeKind( + data.arguments.kind, + getAttributeKindSize(data.arguments.kind, { + max: data.arguments.max, + maxLength: data.arguments.maxLength, + }) + ); + let modifier: { + id: number; + name: string; + size: number; + } | null = null; + if (data.arguments.modifier !== null && data.arguments.size !== null) { + modifier = decodeAttributeModifier( + data.arguments.modifier, + data.arguments.size + ); + } + + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Argument' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + name, + kind, + modifier, + isUpdating: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Argument' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isUpdating: false, + }; + }); + } else { + return items; + } + } + case 'deleteInstructionArgument': + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Argument' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isDeleting: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.filter( + (item) => + !instruction.accounts.some( + (account) => + account.name === 'Argument' && account.pubkey === item.id + ) + ); + } else { + return items; + } + default: + return items; + } +}; diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-collection-instructions.ts b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-collection-instructions.ts new file mode 100644 index 00000000..3336496e --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-collection-instructions.ts @@ -0,0 +1,162 @@ +import { CollectionDto, InstructionStatus } from '@heavy-duty/bulldozer-devkit'; +import { List } from 'immutable'; +import { CollectionItemView } from './types'; + +export const reduceInstructions = ( + items: List, + instruction: InstructionStatus +): List => { + switch (instruction.name) { + case 'createCollection': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: CollectionDto; + }; + const name = data.arguments.name; + + const workspaceId = instruction.accounts.find( + (account) => account.name === 'Workspace' + )?.pubkey; + const applicationId = instruction.accounts.find( + (account) => account.name === 'Application' + )?.pubkey; + const instructionId = instruction.accounts.find( + (account) => account.name === 'Collection' + )?.pubkey; + + if ( + workspaceId === undefined || + applicationId === undefined || + instructionId === undefined + ) { + throw new Error('Malformed Create Collection'); + } + + const itemIndex = items.findIndex((item) => item.id === instructionId); + + if (itemIndex === -1) { + return items.push({ + id: instructionId, + name, + isCreating: true, + isUpdating: false, + isDeleting: false, + applicationId, + workspaceId, + }); + } else { + return items.update(itemIndex, (item) => ({ + ...item, + id: instructionId, + name, + isCreating: true, + isUpdating: false, + isDeleting: false, + applicationId, + workspaceId, + })); + } + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Collection' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isCreating: false, + }; + }); + } else { + return items; + } + } + case 'updateCollection': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: { name: string }; + }; + const name = data.arguments.name; + + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Collection' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + name, + isUpdating: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Collection' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isUpdating: false, + }; + }); + } else { + return items; + } + } + case 'deleteCollection': + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Collection' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isDeleting: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.filter( + (item) => + !instruction.accounts.some( + (account) => + account.name === 'Collection' && account.pubkey === item.id + ) + ); + } else { + return items; + } + default: + return items; + } +}; diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-instructions.ts b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-instructions.ts new file mode 100644 index 00000000..33c0900b --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-instructions.ts @@ -0,0 +1,135 @@ +import { + InstructionBodyDto, + InstructionDto, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { InstructionItemView } from './types'; + +export const reduceInstructions = ( + item: InstructionItemView | null, + instruction: InstructionStatus +): InstructionItemView | null => { + switch (instruction.name) { + case 'createInstruction': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: InstructionDto; + }; + const name = data.arguments.name; + + const workspaceId = instruction.accounts.find( + (account) => account.name === 'Workspace' + )?.pubkey; + const applicationId = instruction.accounts.find( + (account) => account.name === 'Application' + )?.pubkey; + const instructionId = instruction.accounts.find( + (account) => account.name === 'Instruction' + )?.pubkey; + + if ( + workspaceId === undefined || + applicationId === undefined || + instructionId === undefined + ) { + throw new Error('Malformed Create Instruction'); + } + + return { + id: instructionId, + name, + isCreating: true, + isUpdating: false, + isDeleting: false, + applicationId, + workspaceId, + body: '', + }; + } else if (instruction.transactionStatus.status === 'finalized') { + if (item === null) { + return null; + } else { + return { + ...item, + isCreating: false, + }; + } + } else { + return item; + } + } + case 'updateInstruction': { + if (item === null) { + return item; + } else if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: InstructionDto; + }; + const name = data.arguments.name; + + return { + ...item, + name, + isUpdating: true, + }; + } else if (instruction.transactionStatus.status === 'finalized') { + return { + ...item, + isUpdating: false, + }; + } else { + return item; + } + } + case 'updateInstructionBody': { + if (item === null) { + return item; + } else if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: InstructionBodyDto; + }; + const body = data.arguments.body; + + return { + ...item, + body, + isUpdating: true, + }; + } else if (instruction.transactionStatus.status === 'finalized') { + return { + ...item, + isUpdating: false, + }; + } else { + return item; + } + } + case 'deleteInstruction': + if (item === null) { + return item; + } else if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + return { + ...item, + isDeleting: true, + }; + } else if (instruction.transactionStatus.status === 'finalized') { + return null; + } else { + return item; + } + default: + return item; + } +}; diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-relation-instructions.ts b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-relation-instructions.ts new file mode 100644 index 00000000..ec9f2ac8 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/reduce-relation-instructions.ts @@ -0,0 +1,127 @@ +import { InstructionStatus } from '@heavy-duty/bulldozer-devkit'; +import { List } from 'immutable'; +import { InstructionRelationItemView } from './types'; + +export const reduceInstructions = ( + items: List, + instruction: InstructionStatus +): List => { + switch (instruction.name) { + case 'createInstructionRelation': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const workspaceId = instruction.accounts.find( + (account) => account.name === 'Workspace' + )?.pubkey; + const applicationId = instruction.accounts.find( + (account) => account.name === 'Application' + )?.pubkey; + const instructionId = instruction.accounts.find( + (account) => account.name === 'Instruction' + )?.pubkey; + const fromId = instruction.accounts.find( + (account) => account.name === 'From' + )?.pubkey; + const toId = instruction.accounts.find( + (account) => account.name === 'To' + )?.pubkey; + const relationId = instruction.accounts.find( + (account) => account.name === 'Relation' + )?.pubkey; + + if ( + workspaceId === undefined || + applicationId === undefined || + instructionId === undefined || + fromId === undefined || + toId === undefined || + relationId === undefined + ) { + throw new Error('Malformed Create Instruction Relation'); + } + + const itemIndex = items.findIndex((item) => item.id === relationId); + + if (itemIndex === -1) { + return items.push({ + id: relationId, + isCreating: true, + isUpdating: false, + isDeleting: false, + instructionId, + applicationId, + workspaceId, + from: fromId, + to: toId, + }); + } else { + return items.update(itemIndex, (item) => ({ + ...item, + id: relationId, + isCreating: true, + isUpdating: false, + isDeleting: false, + instructionId, + applicationId, + workspaceId, + from: fromId, + to: toId, + })); + } + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Relation' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isCreating: false, + }; + }); + } else { + return items; + } + } + case 'deleteInstructionRelation': + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Relation' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isDeleting: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.filter( + (item) => + !instruction.accounts.some( + (account) => + account.name === 'Relation' && account.pubkey === item.id + ) + ); + } else { + return items; + } + default: + return items; + } +}; diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/types.ts b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/types.ts new file mode 100644 index 00000000..ab886e23 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/types.ts @@ -0,0 +1,94 @@ +export interface CollectionItemView { + id: string; + name: string; + isCreating: boolean; + isUpdating: boolean; + isDeleting: boolean; + applicationId: string; + workspaceId: string; +} + +export interface InstructionAccountItemView { + id: string; + name: string; + kind: { + id: number; + name: string; + }; + modifier: { + id: number; + name: string; + } | null; + space: number | null; + isCreating: boolean; + isUpdating: boolean; + isDeleting: boolean; + instructionId: string; + applicationId: string; + workspaceId: string; + collection: string | null; + close: string | null; + payer: string | null; +} + +export interface InstructionAccountPayerItemView { + id: string; + payer: string | null; + isUpdating: boolean; +} + +export interface InstructionAccountCollectionItemView { + id: string; + collection: string | null; + isUpdating: boolean; +} + +export interface InstructionAccountCloseItemView { + id: string; + close: string | null; + isUpdating: boolean; +} + +export interface InstructionRelationItemView { + id: string; + isCreating: boolean; + isUpdating: boolean; + isDeleting: boolean; + instructionId: string; + applicationId: string; + workspaceId: string; + to: string; + from: string; +} + +export interface InstructionItemView { + id: string; + name: string; + isCreating: boolean; + isUpdating: boolean; + isDeleting: boolean; + applicationId: string; + workspaceId: string; + body: string; +} + +export interface InstructionArgumentItemView { + id: string; + name: string; + kind: { + id: number; + name: string; + size: number; + }; + modifier: { + id: number; + name: string; + size: number; + } | null; + isCreating: boolean; + isUpdating: boolean; + isDeleting: boolean; + instructionId: string; + applicationId: string; + workspaceId: string; +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-accounts.store.ts b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-accounts.store.ts new file mode 100644 index 00000000..d5c64537 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-accounts.store.ts @@ -0,0 +1,205 @@ +import { Injectable } from '@angular/core'; +import { InstructionAccountsStore } from '@bulldozer-client/instructions-data-access'; +import { + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; +import { + Document, + flattenInstructions, + InstructionAccount, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined, isTruthy } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-account-instructions'; +import { InstructionAccountItemView } from './types'; + +const documentToView = ( + instructionAccount: Document +): InstructionAccountItemView => { + return { + id: instructionAccount.id, + name: instructionAccount.name, + isCreating: false, + isUpdating: false, + isDeleting: false, + kind: instructionAccount.data.kind, + modifier: instructionAccount.data.modifier, + instructionId: instructionAccount.data.instruction, + applicationId: instructionAccount.data.application, + workspaceId: instructionAccount.data.workspace, + space: instructionAccount.data.space, + close: instructionAccount.data.close, + collection: instructionAccount.data.collection, + payer: instructionAccount.data.payer, + }; +}; + +interface ViewModel { + instructionId: string | null; + transactions: List; +} + +const initialState: ViewModel = { + instructionId: null, + transactions: List(), +}; + +@Injectable() +export class ViewInstructionCodeEditorAccountsStore extends ComponentStore { + private readonly _instructionId$ = this.select( + ({ instructionId }) => instructionId + ); + private readonly _topicName$ = this.select( + this._instructionId$.pipe(isNotNullOrUndefined), + (instructionId) => `instructions:${instructionId}:accounts` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly accounts$ = this.select( + this._instructionAccountsStore.instructionAccounts$, + this._instructionStatuses$, + (instructionAccounts, instructionStatuses) => { + if (instructionAccounts === null) { + return null; + } + + return instructionStatuses.reduce( + reduceInstructions, + instructionAccounts.map(documentToView) + ); + }, + { debounce: true } + ); + + constructor( + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _instructionAccountsStore: InstructionAccountsStore + ) { + super(initialState); + + this._instructionAccountsStore.setFilters( + this.select( + this._instructionId$.pipe(isNotNullOrUndefined), + this._hdBroadcasterSocketStore.connected$.pipe(isTruthy), + (instructionId) => ({ instruction: instructionId }) + ) + ); + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) + ) + ); + } + + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) + ); + + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) + ); + + readonly setInstructionId = this.updater( + (state, instructionId) => ({ + ...state, + instructionId, + }) + ); + + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); + } + }) + ); + + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; + }>( + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } + + this.patchState({ transactions: List() }); + + const correlationId = uuid(); + let subscriptionId: string; + + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } + + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName + ); + } + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } + }) + ); + }) + ); +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-arguments.store.ts b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-arguments.store.ts new file mode 100644 index 00000000..6d7720d7 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-arguments.store.ts @@ -0,0 +1,201 @@ +import { Injectable } from '@angular/core'; +import { InstructionArgumentsStore } from '@bulldozer-client/instructions-data-access'; +import { + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; +import { + Document, + flattenInstructions, + InstructionArgument, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined, isTruthy } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-argument-instructions'; +import { InstructionArgumentItemView } from './types'; + +const documentToView = ( + document: Document +): InstructionArgumentItemView => { + return { + id: document.id, + name: document.name, + isCreating: false, + isUpdating: false, + isDeleting: false, + kind: document.data.kind, + modifier: document.data.modifier, + instructionId: document.data.instruction, + applicationId: document.data.application, + workspaceId: document.data.workspace, + }; +}; + +interface ViewModel { + instructionId: string | null; + transactions: List; +} + +const initialState: ViewModel = { + instructionId: null, + transactions: List(), +}; + +@Injectable() +export class ViewInstructionCodeEditorArgumentsStore extends ComponentStore { + private readonly _instructionId$ = this.select( + ({ instructionId }) => instructionId + ); + private readonly _topicName$ = this.select( + this._instructionId$.pipe(isNotNullOrUndefined), + (instructionId) => `instructionArguments:${instructionId}` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly instructionArguments$ = this.select( + this._instructionArgumentsStore.instructionArguments$, + this._instructionStatuses$, + (instructionsArguments, instructionStatuses) => { + if (instructionsArguments === null) { + return null; + } + + return instructionStatuses.reduce( + reduceInstructions, + instructionsArguments.map(documentToView) + ); + }, + { debounce: true } + ); + + constructor( + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _instructionArgumentsStore: InstructionArgumentsStore + ) { + super(initialState); + + this._instructionArgumentsStore.setFilters( + this.select( + this._instructionId$.pipe(isNotNullOrUndefined), + this._hdBroadcasterSocketStore.connected$.pipe(isTruthy), + (instructionId) => ({ instruction: instructionId }) + ) + ); + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) + ) + ); + } + + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) + ); + + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) + ); + + readonly setInstructionId = this.updater( + (state, instructionId) => ({ + ...state, + instructionId, + }) + ); + + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); + } + }) + ); + + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; + }>( + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } + + this.patchState({ transactions: List() }); + + const correlationId = uuid(); + let subscriptionId: string; + + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } + + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName + ); + } + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } + }) + ); + }) + ); +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-close-references.store.ts b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-close-references.store.ts new file mode 100644 index 00000000..a87aaa45 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-close-references.store.ts @@ -0,0 +1,202 @@ +import { Injectable } from '@angular/core'; +import { InstructionAccountClosesStore } from '@bulldozer-client/instructions-data-access'; +import { + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; +import { + Document, + flattenInstructions, + InstructionAccountClose, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-account-close-instructions'; +import { InstructionAccountCloseItemView } from './types'; + +const documentToView = ( + instructionAccountClose: Document +): InstructionAccountCloseItemView => { + return { + id: instructionAccountClose.id, + close: instructionAccountClose.data.close, + isUpdating: false, + }; +}; + +interface ViewModel { + instructionId: string | null; + instructionAccountCloseIds: List | null; + transactions: List; +} + +const initialState: ViewModel = { + instructionId: null, + instructionAccountCloseIds: null, + transactions: List(), +}; + +@Injectable() +export class ViewInstructionCodeEditorClosesReferencesStore extends ComponentStore { + private readonly _instructionId$ = this.select( + ({ instructionId }) => instructionId + ); + private readonly _instructionAccountCloseIds$ = this.select( + ({ instructionAccountCloseIds }) => instructionAccountCloseIds + ); + private readonly _topicName$ = this.select( + this._instructionId$.pipe(isNotNullOrUndefined), + (instructionId) => `instructions:${instructionId}:accounts` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly accounts$ = this.select( + this._instructionAccountClosesStore.instructionAccountCloses$, + this._instructionStatuses$, + (instructionAccountCloses, instructionStatuses) => { + if (instructionAccountCloses === null) { + return null; + } + + return instructionStatuses.reduce( + reduceInstructions, + instructionAccountCloses.map(documentToView) + ); + }, + { debounce: true } + ); + + constructor( + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _instructionAccountClosesStore: InstructionAccountClosesStore + ) { + super(initialState); + + this._instructionAccountClosesStore.setInstructionAccountCloseIds( + this._instructionAccountCloseIds$ + ); + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) + ) + ); + } + + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) + ); + + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) + ); + + readonly setInstructionId = this.updater( + (state, instructionId) => ({ + ...state, + instructionId, + }) + ); + + readonly setInstructionAccountCloseIds = this.updater | null>( + (state, instructionAccountCloseIds) => ({ + ...state, + instructionAccountCloseIds, + }) + ); + + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); + } + }) + ); + + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; + }>( + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } + + this.patchState({ transactions: List() }); + + const correlationId = uuid(); + let subscriptionId: string; + + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } + + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName + ); + } + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } + }) + ); + }) + ); +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-collections-references.store.ts b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-collections-references.store.ts new file mode 100644 index 00000000..1cd0182c --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-collections-references.store.ts @@ -0,0 +1,203 @@ +import { Injectable } from '@angular/core'; +import { InstructionAccountCollectionsStore } from '@bulldozer-client/instructions-data-access'; +import { + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; +import { + Document, + flattenInstructions, + InstructionAccountCollection, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-account-collection-instructions'; +import { InstructionAccountCollectionItemView } from './types'; + +const documentToView = ( + instructionAccountCollection: Document +): InstructionAccountCollectionItemView => { + return { + id: instructionAccountCollection.id, + collection: instructionAccountCollection.data.collection, + isUpdating: false, + }; +}; + +interface ViewModel { + instructionId: string | null; + instructionAccountCollectionIds: List | null; + transactions: List; +} + +const initialState: ViewModel = { + instructionId: null, + instructionAccountCollectionIds: null, + transactions: List(), +}; + +@Injectable() +export class ViewInstructionCodeEditorCollectionsReferencesStore extends ComponentStore { + private readonly _instructionId$ = this.select( + ({ instructionId }) => instructionId + ); + private readonly _instructionAccountCollectionIds$ = this.select( + ({ instructionAccountCollectionIds }) => instructionAccountCollectionIds + ); + private readonly _topicName$ = this.select( + this._instructionId$.pipe(isNotNullOrUndefined), + (instructionId) => `instructions:${instructionId}:accounts` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly accounts$ = this.select( + this._instructionAccountCollectionsStore.instructionAccountCollections$, + this._instructionStatuses$, + (instructionAccountCollections, instructionStatuses) => { + if (instructionAccountCollections === null) { + return null; + } + + return instructionStatuses.reduce( + reduceInstructions, + instructionAccountCollections.map(documentToView) + ); + }, + { debounce: true } + ); + + constructor( + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _instructionAccountCollectionsStore: InstructionAccountCollectionsStore + ) { + super(initialState); + + this._instructionAccountCollectionsStore.setInstructionAccountCollectionIds( + this._instructionAccountCollectionIds$ + ); + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) + ) + ); + } + + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) + ); + + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) + ); + + readonly setInstructionId = this.updater( + (state, instructionId) => ({ + ...state, + instructionId, + }) + ); + + readonly setInstructionAccountCollectionIds = + this.updater | null>( + (state, instructionAccountCollectionIds) => ({ + ...state, + instructionAccountCollectionIds, + }) + ); + + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); + } + }) + ); + + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; + }>( + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } + + this.patchState({ transactions: List() }); + + const correlationId = uuid(); + let subscriptionId: string; + + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } + + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName + ); + } + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } + }) + ); + }) + ); +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-collections.store.ts b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-collections.store.ts new file mode 100644 index 00000000..bd8bd1e4 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-collections.store.ts @@ -0,0 +1,191 @@ +import { Injectable } from '@angular/core'; +import { CollectionsStore } from '@bulldozer-client/collections-data-access'; +import { + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; +import { + Collection, + Document, + flattenInstructions, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined, isTruthy } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-collection-instructions'; +import { CollectionItemView } from './types'; + +const documentToView = (document: Document): CollectionItemView => { + return { + id: document.id, + name: document.name, + isCreating: false, + isUpdating: false, + isDeleting: false, + applicationId: document.data.application, + workspaceId: document.data.workspace, + }; +}; + +interface ViewModel { + applicationId: string | null; + transactions: List; +} + +const initialState: ViewModel = { + applicationId: null, + transactions: List(), +}; + +@Injectable() +export class ViewInstructionCodeEditorCollectionsStore extends ComponentStore { + readonly applicationId$ = this.select(({ applicationId }) => applicationId); + private readonly _topicName$ = this.select( + this.applicationId$.pipe(isNotNullOrUndefined), + (applicationId) => `applications:${applicationId}:collections` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly collections$ = this.select( + this._collectionsStore.collections$, + this._instructionStatuses$, + (collections, instructionStatuses) => { + if (collections === null) { + return null; + } + + return instructionStatuses.reduce( + reduceInstructions, + collections.map(documentToView) + ); + }, + { debounce: true } + ); + + constructor( + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _collectionsStore: CollectionsStore + ) { + super(initialState); + + this._collectionsStore.setFilters( + this.select( + this.applicationId$.pipe(isNotNullOrUndefined), + this._hdBroadcasterSocketStore.connected$.pipe(isTruthy), + (applicationId) => ({ application: applicationId }) + ) + ); + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) + ) + ); + } + + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) + ); + + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) + ); + + readonly setApplicationId = this.updater( + (state, applicationId) => ({ ...state, applicationId }) + ); + + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); + } + }) + ); + + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; + }>( + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } + + this.patchState({ transactions: List() }); + + const correlationId = uuid(); + let subscriptionId: string; + + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } + + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName + ); + } + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } + }) + ); + }) + ); +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-instruction.store.ts b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-instruction.store.ts new file mode 100644 index 00000000..dff95301 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-instruction.store.ts @@ -0,0 +1,192 @@ +import { Injectable } from '@angular/core'; +import { InstructionStore } from '@bulldozer-client/instructions-data-access'; +import { + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; +import { + Document, + flattenInstructions, + Instruction, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined, isTruthy } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-instructions'; +import { InstructionItemView } from './types'; + +const documentToView = ( + document: Document +): InstructionItemView => { + return { + id: document.id, + name: document.name, + isCreating: false, + isUpdating: false, + isDeleting: false, + applicationId: document.data.application, + workspaceId: document.data.workspace, + body: document.data.body, + }; +}; + +interface ViewModel { + instructionId: string | null; + transactions: List; +} + +const initialState: ViewModel = { + instructionId: null, + transactions: List(), +}; + +@Injectable() +export class ViewInstructionCodeEditorInstructionStore extends ComponentStore { + readonly instructionId$ = this.select(({ instructionId }) => instructionId); + private readonly _topicName$ = this.select( + this.instructionId$.pipe(isNotNullOrUndefined), + (instructionId) => `instructions:${instructionId}` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly instruction$ = this.select( + this._instructionStore.instruction$, + this._instructionStatuses$, + (instruction, instructionStatuses) => + instructionStatuses.reduce( + reduceInstructions, + instruction === null ? null : documentToView(instruction) + ), + { debounce: true } + ); + + constructor( + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _instructionStore: InstructionStore + ) { + super(initialState); + + this._instructionStore.setInstructionId( + this.select( + this.instructionId$.pipe(isNotNullOrUndefined), + this._hdBroadcasterSocketStore.connected$.pipe(isTruthy), + (instructionId) => instructionId + ) + ); + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) + ) + ); + } + + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) + ); + + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) + ); + + readonly setInstructionId = this.updater( + (state, instructionId) => ({ + ...state, + instructionId, + }) + ); + + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); + } + }) + ); + + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; + }>( + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } + + this.patchState({ transactions: List() }); + + const correlationId = uuid(); + let subscriptionId: string; + + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } + + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName + ); + } + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } + }) + ); + }) + ); +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-payers-references.store.ts b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-payers-references.store.ts new file mode 100644 index 00000000..c97ee640 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-payers-references.store.ts @@ -0,0 +1,202 @@ +import { Injectable } from '@angular/core'; +import { InstructionAccountPayersStore } from '@bulldozer-client/instructions-data-access'; +import { + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; +import { + Document, + flattenInstructions, + InstructionAccountPayer, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-account-payer-instructions'; +import { InstructionAccountPayerItemView } from './types'; + +const documentToView = ( + instructionAccountPayer: Document +): InstructionAccountPayerItemView => { + return { + id: instructionAccountPayer.id, + payer: instructionAccountPayer.data.payer, + isUpdating: false, + }; +}; + +interface ViewModel { + instructionId: string | null; + instructionAccountPayerIds: List | null; + transactions: List; +} + +const initialState: ViewModel = { + instructionId: null, + instructionAccountPayerIds: null, + transactions: List(), +}; + +@Injectable() +export class ViewInstructionCodeEditorPayersReferencesStore extends ComponentStore { + private readonly _instructionId$ = this.select( + ({ instructionId }) => instructionId + ); + private readonly _instructionAccountPayerIds$ = this.select( + ({ instructionAccountPayerIds }) => instructionAccountPayerIds + ); + private readonly _topicName$ = this.select( + this._instructionId$.pipe(isNotNullOrUndefined), + (instructionId) => `instructions:${instructionId}:accounts` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly accounts$ = this.select( + this._instructionAccountPayersStore.instructionAccountPayers$, + this._instructionStatuses$, + (instructionAccountPayers, instructionStatuses) => { + if (instructionAccountPayers === null) { + return null; + } + + return instructionStatuses.reduce( + reduceInstructions, + instructionAccountPayers.map(documentToView) + ); + }, + { debounce: true } + ); + + constructor( + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _instructionAccountPayersStore: InstructionAccountPayersStore + ) { + super(initialState); + + this._instructionAccountPayersStore.setInstructionAccountPayerIds( + this._instructionAccountPayerIds$ + ); + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) + ) + ); + } + + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) + ); + + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) + ); + + readonly setInstructionId = this.updater( + (state, instructionId) => ({ + ...state, + instructionId, + }) + ); + + readonly setInstructionAccountPayerIds = this.updater | null>( + (state, instructionAccountPayerIds) => ({ + ...state, + instructionAccountPayerIds, + }) + ); + + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); + } + }) + ); + + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; + }>( + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } + + this.patchState({ transactions: List() }); + + const correlationId = uuid(); + let subscriptionId: string; + + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } + + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName + ); + } + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } + }) + ); + }) + ); +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-relations.store.ts b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-relations.store.ts new file mode 100644 index 00000000..947202f7 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor-relations.store.ts @@ -0,0 +1,200 @@ +import { Injectable } from '@angular/core'; +import { InstructionRelationsStore } from '@bulldozer-client/instructions-data-access'; +import { + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; +import { + flattenInstructions, + InstructionRelation, + InstructionStatus, + Relation, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined, isTruthy } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-relation-instructions'; +import { InstructionRelationItemView } from './types'; + +const documentToView = ( + instructionRelation: Relation +): InstructionRelationItemView => { + return { + id: instructionRelation.id, + isCreating: false, + isUpdating: false, + isDeleting: false, + to: instructionRelation.to, + from: instructionRelation.from, + instructionId: instructionRelation.data.instruction, + applicationId: instructionRelation.data.application, + workspaceId: instructionRelation.data.workspace, + }; +}; + +interface ViewModel { + instructionId: string | null; + transactions: List; +} + +const initialState: ViewModel = { + instructionId: null, + transactions: List(), +}; + +@Injectable() +export class ViewInstructionCodeEditorRelationsStore extends ComponentStore { + private readonly _instructionId$ = this.select( + ({ instructionId }) => instructionId + ); + private readonly _topicName$ = this.select( + this._instructionId$.pipe(isNotNullOrUndefined), + (instructionId) => `instructions:${instructionId}:accounts` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly accounts$ = this.select( + this._instructionRelationsStore.instructionRelations$, + this._instructionStatuses$, + (instructionRelations, instructionStatuses) => { + if (instructionRelations === null) { + return null; + } + + return instructionStatuses.reduce( + reduceInstructions, + instructionRelations.map(documentToView) + ); + }, + { debounce: true } + ); + + constructor( + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _instructionRelationsStore: InstructionRelationsStore + ) { + super(initialState); + + this._instructionRelationsStore.setFilters( + this.select( + this._instructionId$.pipe(isNotNullOrUndefined), + this._hdBroadcasterSocketStore.connected$.pipe(isTruthy), + (instructionId) => ({ instruction: instructionId }) + ) + ); + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) + ) + ); + } + + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) + ); + + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) + ); + + readonly setInstructionId = this.updater( + (state, instructionId) => ({ + ...state, + instructionId, + }) + ); + + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); + } + }) + ); + + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; + }>( + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } + + this.patchState({ transactions: List() }); + + const correlationId = uuid(); + let subscriptionId: string; + + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } + + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName + ); + } + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } + }) + ); + }) + ); +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor.component.ts b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor.component.ts new file mode 100644 index 00000000..deaaac12 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor.component.ts @@ -0,0 +1,266 @@ +import { + ChangeDetectionStrategy, + Component, + HostBinding, + OnInit, +} from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { CollectionsStore } from '@bulldozer-client/collections-data-access'; +import { + InstructionAccountClosesStore, + InstructionAccountCollectionsStore, + InstructionAccountPayersStore, + InstructionAccountQueryStore, + InstructionAccountsStore, + InstructionApiService, + InstructionArgumentQueryStore, + InstructionArgumentsStore, + InstructionRelationQueryStore, + InstructionRelationsStore, + InstructionStore, +} from '@bulldozer-client/instructions-data-access'; +import { NotificationStore } from '@bulldozer-client/notifications-data-access'; +import { HdBroadcasterSocketStore } from '@heavy-duty/broadcaster'; +import { InstructionBodyDto } from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { WalletStore } from '@heavy-duty/wallet-adapter'; +import { distinctUntilChanged, map } from 'rxjs'; +import { ViewInstructionCodeEditorAccountsStore } from './view-instruction-code-editor-accounts.store'; +import { ViewInstructionCodeEditorArgumentsStore } from './view-instruction-code-editor-arguments.store'; +import { ViewInstructionCodeEditorClosesReferencesStore } from './view-instruction-code-editor-close-references.store'; +import { ViewInstructionCodeEditorCollectionsReferencesStore } from './view-instruction-code-editor-collections-references.store'; +import { ViewInstructionCodeEditorCollectionsStore } from './view-instruction-code-editor-collections.store'; +import { ViewInstructionCodeEditorInstructionStore } from './view-instruction-code-editor-instruction.store'; +import { ViewInstructionCodeEditorPayersReferencesStore } from './view-instruction-code-editor-payers-references.store'; +import { ViewInstructionCodeEditorRelationsStore } from './view-instruction-code-editor-relations.store'; +import { ViewInstructionCodeEditorStore } from './view-instruction-code-editor.store'; + +@Component({ + selector: 'bd-view-instruction-code-editor', + template: ` +
+

Code Editor

+

+ The code editor allows you to customize an instruction. +

+
+ +
+
+ + + + +
+ +

+ report + You have unsaved changes. Remember to + +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `, + styles: [], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + InstructionAccountPayersStore, + InstructionAccountClosesStore, + InstructionAccountCollectionsStore, + InstructionStore, + InstructionArgumentsStore, + InstructionArgumentQueryStore, + InstructionAccountsStore, + InstructionAccountQueryStore, + InstructionRelationsStore, + InstructionRelationQueryStore, + CollectionsStore, + ViewInstructionCodeEditorStore, + ViewInstructionCodeEditorInstructionStore, + ViewInstructionCodeEditorAccountsStore, + ViewInstructionCodeEditorRelationsStore, + ViewInstructionCodeEditorCollectionsStore, + ViewInstructionCodeEditorPayersReferencesStore, + ViewInstructionCodeEditorCollectionsReferencesStore, + ViewInstructionCodeEditorClosesReferencesStore, + ViewInstructionCodeEditorArgumentsStore, + ], +}) +export class ViewInstructionCodeEditorComponent implements OnInit { + @HostBinding('class') class = 'flex flex-col p-8 pt-5 h-full'; + instructionBody: string | null = null; + + readonly contextCode$ = this._viewInstructionCodeEditorStore.contextCode$; + readonly handleCode$ = this._viewInstructionCodeEditorStore.handleCode$; + readonly instruction$ = + this._viewInstructionCodeEditorInstructionStore.instruction$; + readonly workspaceId$ = this._route.paramMap.pipe( + map((paramMap) => paramMap.get('workspaceId')), + isNotNullOrUndefined, + distinctUntilChanged() + ); + readonly applicationId$ = this._route.paramMap.pipe( + map((paramMap) => paramMap.get('applicationId')), + isNotNullOrUndefined, + distinctUntilChanged() + ); + readonly instructionId$ = this._route.paramMap.pipe( + map((paramMap) => paramMap.get('instructionId')), + isNotNullOrUndefined, + distinctUntilChanged() + ); + + constructor( + private readonly _route: ActivatedRoute, + private readonly _walletStore: WalletStore, + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _notificationStore: NotificationStore, + private readonly _instructionApiService: InstructionApiService, + private readonly _viewInstructionCodeEditorStore: ViewInstructionCodeEditorStore, + private readonly _viewInstructionCodeEditorInstructionStore: ViewInstructionCodeEditorInstructionStore, + private readonly _viewInstructionCodeEditorArgumentsStore: ViewInstructionCodeEditorArgumentsStore, + private readonly _viewInstructionCodeEditorAccountsStore: ViewInstructionCodeEditorAccountsStore, + private readonly _viewInstructionCodeEditorRelationsStore: ViewInstructionCodeEditorRelationsStore, + private readonly _viewInstructionCodeEditorCollectionsStore: ViewInstructionCodeEditorCollectionsStore, + private readonly _viewInstructionCodeEditorPayersReferencesStore: ViewInstructionCodeEditorPayersReferencesStore, + private readonly _viewInstructionCodeEditorCollectionsReferencesStore: ViewInstructionCodeEditorCollectionsReferencesStore, + private readonly _viewInstructionCodeEditorClosesReferencesStore: ViewInstructionCodeEditorClosesReferencesStore + ) {} + + ngOnInit() { + this._viewInstructionCodeEditorAccountsStore.setInstructionId( + this.instructionId$ + ); + this._viewInstructionCodeEditorRelationsStore.setInstructionId( + this.instructionId$ + ); + this._viewInstructionCodeEditorPayersReferencesStore.setInstructionId( + this.instructionId$ + ); + this._viewInstructionCodeEditorCollectionsReferencesStore.setInstructionId( + this.instructionId$ + ); + this._viewInstructionCodeEditorClosesReferencesStore.setInstructionId( + this.instructionId$ + ); + this._viewInstructionCodeEditorCollectionsStore.setApplicationId( + this.applicationId$ + ); + this._viewInstructionCodeEditorInstructionStore.setInstructionId( + this.instructionId$ + ); + this._viewInstructionCodeEditorArgumentsStore.setInstructionId( + this.instructionId$ + ); + } + + onUpdateInstructionBody( + authority: string, + workspaceId: string, + applicationId: string, + instructionId: string, + instructionBodyDto: InstructionBodyDto + ) { + this._instructionApiService + .updateBody({ + authority, + workspaceId, + applicationId, + instructionId, + instructionBodyDto, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent( + 'Update instruction body request sent' + ); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `instructions:${instructionId}`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); + } +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor.module.ts b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor.module.ts new file mode 100644 index 00000000..7ea0c305 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor.module.ts @@ -0,0 +1,29 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { RouterModule } from '@angular/router'; +import { CodeEditorModule } from '@bulldozer-client/code-editor'; +import { HdWalletAdapterCdkModule } from '@heavy-duty/wallet-adapter-cdk'; +import { ReactiveComponentModule } from '@ngrx/component'; +import { ViewInstructionCodeEditorComponent } from './view-instruction-code-editor.component'; + +@NgModule({ + imports: [ + CommonModule, + RouterModule.forChild([ + { + path: '', + pathMatch: 'full', + component: ViewInstructionCodeEditorComponent, + }, + ]), + MatButtonModule, + MatIconModule, + ReactiveComponentModule, + CodeEditorModule, + HdWalletAdapterCdkModule, + ], + declarations: [ViewInstructionCodeEditorComponent], +}) +export class ViewInstructionCodeEditorModule {} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor.store.ts b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor.store.ts new file mode 100644 index 00000000..7c0206b0 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/lib/view-instruction-code-editor.store.ts @@ -0,0 +1,137 @@ +import { Injectable } from '@angular/core'; +import { InstructionAccountsStore } from '@bulldozer-client/instructions-data-access'; +import { generateInstructionCode2 } from '@heavy-duty/generator'; +import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { map } from 'rxjs'; +import { ViewInstructionCodeEditorAccountsStore } from './view-instruction-code-editor-accounts.store'; +import { ViewInstructionCodeEditorArgumentsStore } from './view-instruction-code-editor-arguments.store'; +import { ViewInstructionCodeEditorClosesReferencesStore } from './view-instruction-code-editor-close-references.store'; +import { ViewInstructionCodeEditorCollectionsReferencesStore } from './view-instruction-code-editor-collections-references.store'; +import { ViewInstructionCodeEditorCollectionsStore } from './view-instruction-code-editor-collections.store'; +import { ViewInstructionCodeEditorInstructionStore } from './view-instruction-code-editor-instruction.store'; +import { ViewInstructionCodeEditorPayersReferencesStore } from './view-instruction-code-editor-payers-references.store'; +import { ViewInstructionCodeEditorRelationsStore } from './view-instruction-code-editor-relations.store'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface ViewModel {} + +const initialState: ViewModel = {}; + +@Injectable() +export class ViewInstructionCodeEditorStore extends ComponentStore { + readonly contextCode$ = this.select( + this._viewInstructionCodeEditorInstructionStore.instruction$, + this._viewInstructionCodeEditorArgumentsStore.instructionArguments$, + this._viewInstructionCodeEditorAccountsStore.accounts$, + this._viewInstructionCodeEditorCollectionsStore.collections$, + this._viewInstructionCodeEditorPayersReferencesStore.accounts$, + this._viewInstructionCodeEditorCollectionsReferencesStore.accounts$, + this._viewInstructionCodeEditorClosesReferencesStore.accounts$, + this._viewInstructionCodeEditorRelationsStore.accounts$, + ( + instruction, + instructionArguments, + instructionAccounts, + collections, + payers, + collectionsReferences, + closes, + relations + ) => { + if ( + instruction === null || + instructionArguments === null || + instructionAccounts === null || + collections === null || + payers === null || + collectionsReferences === null || + closes === null || + relations === null + ) { + return null; + } + + return generateInstructionCode2( + instruction, + instructionArguments, + instructionAccounts.map((instructionAccount) => { + const payerAccountId = + payers.find((payer) => payer.id === instructionAccount.payer) + ?.payer ?? null; + const closeAccountId = + closes.find((close) => close.id === instructionAccount.close) + ?.close ?? null; + const collectionId = + collectionsReferences.find( + (collection) => collection.id === instructionAccount.collection + )?.collection ?? null; + + return { + ...instructionAccount, + payer: payerAccountId, + close: closeAccountId, + collection: collectionId, + }; + }), + relations, + collections + ); + } + ); + readonly handleCode$ = this.select( + this._viewInstructionCodeEditorInstructionStore.instruction$, + (instruction) => instruction?.body ?? null + ); + + constructor( + private readonly _viewInstructionCodeEditorArgumentsStore: ViewInstructionCodeEditorArgumentsStore, + private readonly _viewInstructionCodeEditorInstructionStore: ViewInstructionCodeEditorInstructionStore, + private readonly _viewInstructionCodeEditorCollectionsStore: ViewInstructionCodeEditorCollectionsStore, + private readonly _viewInstructionCodeEditorAccountsStore: ViewInstructionCodeEditorAccountsStore, + private readonly _viewInstructionCodeEditorRelationsStore: ViewInstructionCodeEditorRelationsStore, + private readonly _instructionAccountsStore: InstructionAccountsStore, + private readonly _viewInstructionCodeEditorPayersReferencesStore: ViewInstructionCodeEditorPayersReferencesStore, + private readonly _viewInstructionCodeEditorCollectionsReferencesStore: ViewInstructionCodeEditorCollectionsReferencesStore, + private readonly _viewInstructionCodeEditorClosesReferencesStore: ViewInstructionCodeEditorClosesReferencesStore + ) { + super(initialState); + + this._viewInstructionCodeEditorPayersReferencesStore.setInstructionAccountPayerIds( + this._instructionAccountsStore.instructionAccounts$.pipe( + isNotNullOrUndefined, + map((accounts) => + accounts + .filter((account) => account.data.kind.id === 0) + .map((account) => account.data.payer) + .filter((payer): payer is string => payer !== null) + .toList() + ) + ) + ); + this._viewInstructionCodeEditorCollectionsReferencesStore.setInstructionAccountCollectionIds( + this._instructionAccountsStore.instructionAccounts$.pipe( + isNotNullOrUndefined, + map((accounts) => + accounts + .filter((account) => account.data.kind.id === 0) + .map((account) => account.data.collection) + .filter((collection): collection is string => collection !== null) + .toList() + ) + ) + ); + this._viewInstructionCodeEditorClosesReferencesStore.setInstructionAccountCloseIds( + this._instructionAccountsStore.instructionAccounts$.pipe( + isNotNullOrUndefined, + map((accounts) => + accounts + .filter((account) => account.data.kind.id === 0) + .map((account) => account.data.close) + .filter((close): close is string => close !== null) + .toList() + ) + ) + ); + } +} diff --git a/libs/bulldozer/client/instructions/components/instruction-signers-list/src/test-setup.ts b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/test-setup.ts similarity index 100% rename from libs/bulldozer/client/instructions/components/instruction-signers-list/src/test-setup.ts rename to libs/bulldozer/client/instructions/pages/view-instruction-code-editor/src/test-setup.ts diff --git a/libs/bulldozer/client/instructions/components/instruction-documents-list/tsconfig.json b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/tsconfig.json similarity index 100% rename from libs/bulldozer/client/instructions/components/instruction-documents-list/tsconfig.json rename to libs/bulldozer/client/instructions/pages/view-instruction-code-editor/tsconfig.json diff --git a/libs/bulldozer/client/instructions/components/instruction-documents-list/tsconfig.lib.json b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/tsconfig.lib.json similarity index 100% rename from libs/bulldozer/client/instructions/components/instruction-documents-list/tsconfig.lib.json rename to libs/bulldozer/client/instructions/pages/view-instruction-code-editor/tsconfig.lib.json diff --git a/libs/bulldozer/client/instructions/components/instruction-documents-list/tsconfig.spec.json b/libs/bulldozer/client/instructions/pages/view-instruction-code-editor/tsconfig.spec.json similarity index 100% rename from libs/bulldozer/client/instructions/components/instruction-documents-list/tsconfig.spec.json rename to libs/bulldozer/client/instructions/pages/view-instruction-code-editor/tsconfig.spec.json diff --git a/libs/bulldozer/client/instructions/components/instruction-signers-list/.eslintrc.json b/libs/bulldozer/client/instructions/pages/view-instruction-documents/.eslintrc.json similarity index 100% rename from libs/bulldozer/client/instructions/components/instruction-signers-list/.eslintrc.json rename to libs/bulldozer/client/instructions/pages/view-instruction-documents/.eslintrc.json diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-documents/README.md b/libs/bulldozer/client/instructions/pages/view-instruction-documents/README.md new file mode 100644 index 00000000..93be0cb9 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-documents/README.md @@ -0,0 +1,7 @@ +# bulldozer-client-instructions-pages-view-instruction-documents + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test bulldozer-client-instructions-pages-view-instruction-documents` to execute the unit tests. diff --git a/libs/bulldozer/client/instructions/components/instruction-arguments-list/jest.config.js b/libs/bulldozer/client/instructions/pages/view-instruction-documents/jest.config.js similarity index 83% rename from libs/bulldozer/client/instructions/components/instruction-arguments-list/jest.config.js rename to libs/bulldozer/client/instructions/pages/view-instruction-documents/jest.config.js index 8afd7eb9..8af850a6 100644 --- a/libs/bulldozer/client/instructions/components/instruction-arguments-list/jest.config.js +++ b/libs/bulldozer/client/instructions/pages/view-instruction-documents/jest.config.js @@ -1,6 +1,5 @@ module.exports = { - displayName: - 'bulldozer-client-instructions-components-instruction-arguments-list', + displayName: 'bulldozer-client-instructions-pages-view-instruction-documents', preset: '../../../../../../jest.preset.js', setupFilesAfterEnv: ['/src/test-setup.ts'], globals: { @@ -10,7 +9,7 @@ module.exports = { }, }, coverageDirectory: - '../../../../../../coverage/libs/bulldozer/client/instructions/components/instruction-arguments-list', + '../../../../../../coverage/libs/bulldozer/client/instructions/pages/view-instruction-documents', transform: { '^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular', }, diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-documents/project.json b/libs/bulldozer/client/instructions/pages/view-instruction-documents/project.json new file mode 100644 index 00000000..92d5a48d --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-documents/project.json @@ -0,0 +1,28 @@ +{ + "projectType": "library", + "root": "libs/bulldozer/client/instructions/pages/view-instruction-documents", + "sourceRoot": "libs/bulldozer/client/instructions/pages/view-instruction-documents/src", + "prefix": "bd", + "targets": { + "test": { + "executor": "@nrwl/jest:jest", + "outputs": [ + "coverage/libs/bulldozer/client/instructions/pages/view-instruction-documents" + ], + "options": { + "jestConfig": "libs/bulldozer/client/instructions/pages/view-instruction-documents/jest.config.js", + "passWithNoTests": true + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "libs/bulldozer/client/instructions/pages/view-instruction-documents/src/**/*.ts", + "libs/bulldozer/client/instructions/pages/view-instruction-documents/src/**/*.html" + ] + } + } + }, + "tags": [] +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/index.ts b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/index.ts new file mode 100644 index 00000000..378157de --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/index.ts @@ -0,0 +1 @@ +export * from './lib/view-instruction-documents.module'; diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/reduce-account-close-instructions.ts b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/reduce-account-close-instructions.ts new file mode 100644 index 00000000..7f3cb7f7 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/reduce-account-close-instructions.ts @@ -0,0 +1,68 @@ +import { InstructionStatus } from '@heavy-duty/bulldozer-devkit'; +import { List } from 'immutable'; +import { InstructionAccountCloseItemView } from './types'; + +export const reduceInstructions = ( + items: List, + instruction: InstructionStatus +): List => { + switch (instruction.name) { + case 'setInstructionAccountClose': { + const accountClose = instruction.accounts.find( + (account) => account.name === 'Account Close' + )?.pubkey; + const close = instruction.accounts.find( + (account) => account.name === 'Close' + )?.pubkey; + + if (close === undefined || accountClose === undefined) { + throw new Error('Malformed Set Instruction Account Close'); + } + + const itemIndex = items.findIndex((item) => item.id === accountClose); + + if (itemIndex === -1) { + return items.push({ + id: accountClose, + isUpdating: false, + close, + }); + } else { + return items.update(itemIndex, (item) => ({ + ...item, + id: accountClose, + isUpdating: false, + close, + })); + } + } + case 'clearInstructionAccountClose': { + const accountClose = instruction.accounts.find( + (account) => account.name === 'Account Close' + )?.pubkey; + + if (accountClose === undefined) { + throw new Error('Malformed Clear Instruction Account Close'); + } + + const itemIndex = items.findIndex((item) => item.id === accountClose); + + if (itemIndex === -1) { + return items.push({ + id: accountClose, + isUpdating: false, + close: null, + }); + } else { + return items.update(itemIndex, (item) => ({ + ...item, + id: accountClose, + isUpdating: false, + close: null, + })); + } + } + default: + return items; + } +}; diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/reduce-account-collection-instructions.ts b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/reduce-account-collection-instructions.ts new file mode 100644 index 00000000..1656e7ca --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/reduce-account-collection-instructions.ts @@ -0,0 +1,44 @@ +import { InstructionStatus } from '@heavy-duty/bulldozer-devkit'; +import { List } from 'immutable'; +import { InstructionAccountCollectionItemView } from './types'; + +export const reduceInstructions = ( + items: List, + instruction: InstructionStatus +): List => { + switch (instruction.name) { + case 'setInstructionAccountCollection': { + const accountCollection = instruction.accounts.find( + (account) => account.name === 'Account Collection' + )?.pubkey; + const collection = instruction.accounts.find( + (account) => account.name === 'Collection' + )?.pubkey; + + if (collection === undefined || accountCollection === undefined) { + throw new Error('Malformed Set Instruction Account Collection'); + } + + const itemIndex = items.findIndex( + (item) => item.id === accountCollection + ); + + if (itemIndex === -1) { + return items.push({ + id: accountCollection, + isUpdating: false, + collection, + }); + } else { + return items.update(itemIndex, (item) => ({ + ...item, + id: accountCollection, + isUpdating: false, + collection, + })); + } + } + default: + return items; + } +}; diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/reduce-account-instructions.ts b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/reduce-account-instructions.ts new file mode 100644 index 00000000..9cefa88a --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/reduce-account-instructions.ts @@ -0,0 +1,253 @@ +import { + InstructionAccountDto, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { List } from 'immutable'; +import { InstructionAccountItemView } from './types'; + +const decodeAccountKind = (id: number): { id: number; name: string } => { + switch (id) { + case 0: { + return { + id, + name: 'document', + }; + } + case 1: + return { + id, + name: 'signer', + }; + default: + throw Error('Invalid kind id'); + } +}; + +const decodeAccountModifier = ( + id: number +): { + id: number; + name: string; +} => { + switch (id) { + case 0: { + return { + id, + name: 'init', + }; + } + case 1: + return { + id, + name: 'mut', + }; + default: + throw Error('Invalid kind id'); + } +}; + +export const reduceInstructions = ( + items: List, + instruction: InstructionStatus +): List => { + switch (instruction.name) { + case 'createInstructionAccount': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: InstructionAccountDto; + }; + const name = data.arguments.name; + const space = data.arguments.space; + const kind = decodeAccountKind(data.arguments.kind); + let modifier: { + id: number; + name: string; + } | null = null; + if (data.arguments.modifier !== null) { + modifier = decodeAccountModifier(data.arguments.modifier); + } + + const workspaceId = instruction.accounts.find( + (account) => account.name === 'Workspace' + )?.pubkey; + const applicationId = instruction.accounts.find( + (account) => account.name === 'Application' + )?.pubkey; + const instructionId = instruction.accounts.find( + (account) => account.name === 'Instruction' + )?.pubkey; + const accountId = instruction.accounts.find( + (account) => account.name === 'Account' + )?.pubkey; + const collection = + instruction.accounts.find( + (account) => account.name === 'Account Collection' + )?.pubkey ?? null; + const close = + instruction.accounts.find( + (account) => account.name === 'Account Close' + )?.pubkey ?? null; + const payer = + instruction.accounts.find( + (account) => account.name === 'Account Payer' + )?.pubkey ?? null; + + if ( + workspaceId === undefined || + applicationId === undefined || + instructionId === undefined || + accountId === undefined + ) { + throw new Error('Malformed Create Instruction Account'); + } + + const itemIndex = items.findIndex((item) => item.id === accountId); + + if (itemIndex === -1) { + return items.push({ + id: accountId, + name, + kind, + modifier, + isCreating: true, + isUpdating: false, + isDeleting: false, + instructionId, + applicationId, + workspaceId, + space, + collection, + close, + payer, + }); + } else { + return items.update(itemIndex, (item) => ({ + ...item, + id: accountId, + name, + kind, + modifier, + isCreating: true, + isUpdating: false, + isDeleting: false, + instructionId, + applicationId, + workspaceId, + space, + collection, + close, + payer, + })); + } + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Account' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isCreating: false, + }; + }); + } else { + return items; + } + } + case 'updateInstructionAccount': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: InstructionAccountDto; + }; + const name = data.arguments.name; + let modifier: { + id: number; + name: string; + } | null = null; + if (data.arguments.modifier !== null) { + modifier = decodeAccountModifier(data.arguments.modifier); + } + + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Account' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + name, + modifier, + isUpdating: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Account' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isUpdating: false, + }; + }); + } else { + return items; + } + } + case 'deleteInstructionAccount': + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Account' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isDeleting: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.filter( + (item) => + !instruction.accounts.some( + (account) => + account.name === 'Account' && account.pubkey === item.id + ) + ); + } else { + return items; + } + default: + return items; + } +}; diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/reduce-account-payer-instructions.ts b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/reduce-account-payer-instructions.ts new file mode 100644 index 00000000..c7632a5c --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/reduce-account-payer-instructions.ts @@ -0,0 +1,42 @@ +import { InstructionStatus } from '@heavy-duty/bulldozer-devkit'; +import { List } from 'immutable'; +import { InstructionAccountPayerItemView } from './types'; + +export const reduceInstructions = ( + items: List, + instruction: InstructionStatus +): List => { + switch (instruction.name) { + case 'setInstructionAccountPayer': { + const accountPayer = instruction.accounts.find( + (account) => account.name === 'Account Payer' + )?.pubkey; + const payer = instruction.accounts.find( + (account) => account.name === 'Payer' + )?.pubkey; + + if (payer === undefined || accountPayer === undefined) { + throw new Error('Malformed Set Instruction Account Payer'); + } + + const itemIndex = items.findIndex((item) => item.id === accountPayer); + + if (itemIndex === -1) { + return items.push({ + id: accountPayer, + isUpdating: false, + payer, + }); + } else { + return items.update(itemIndex, (item) => ({ + ...item, + id: accountPayer, + isUpdating: false, + payer, + })); + } + } + default: + return items; + } +}; diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/reduce-collection-instructions.ts b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/reduce-collection-instructions.ts new file mode 100644 index 00000000..3336496e --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/reduce-collection-instructions.ts @@ -0,0 +1,162 @@ +import { CollectionDto, InstructionStatus } from '@heavy-duty/bulldozer-devkit'; +import { List } from 'immutable'; +import { CollectionItemView } from './types'; + +export const reduceInstructions = ( + items: List, + instruction: InstructionStatus +): List => { + switch (instruction.name) { + case 'createCollection': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: CollectionDto; + }; + const name = data.arguments.name; + + const workspaceId = instruction.accounts.find( + (account) => account.name === 'Workspace' + )?.pubkey; + const applicationId = instruction.accounts.find( + (account) => account.name === 'Application' + )?.pubkey; + const instructionId = instruction.accounts.find( + (account) => account.name === 'Collection' + )?.pubkey; + + if ( + workspaceId === undefined || + applicationId === undefined || + instructionId === undefined + ) { + throw new Error('Malformed Create Collection'); + } + + const itemIndex = items.findIndex((item) => item.id === instructionId); + + if (itemIndex === -1) { + return items.push({ + id: instructionId, + name, + isCreating: true, + isUpdating: false, + isDeleting: false, + applicationId, + workspaceId, + }); + } else { + return items.update(itemIndex, (item) => ({ + ...item, + id: instructionId, + name, + isCreating: true, + isUpdating: false, + isDeleting: false, + applicationId, + workspaceId, + })); + } + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Collection' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isCreating: false, + }; + }); + } else { + return items; + } + } + case 'updateCollection': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: { name: string }; + }; + const name = data.arguments.name; + + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Collection' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + name, + isUpdating: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Collection' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isUpdating: false, + }; + }); + } else { + return items; + } + } + case 'deleteCollection': + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Collection' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isDeleting: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.filter( + (item) => + !instruction.accounts.some( + (account) => + account.name === 'Collection' && account.pubkey === item.id + ) + ); + } else { + return items; + } + default: + return items; + } +}; diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/reduce-relation-instructions.ts b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/reduce-relation-instructions.ts new file mode 100644 index 00000000..ec9f2ac8 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/reduce-relation-instructions.ts @@ -0,0 +1,127 @@ +import { InstructionStatus } from '@heavy-duty/bulldozer-devkit'; +import { List } from 'immutable'; +import { InstructionRelationItemView } from './types'; + +export const reduceInstructions = ( + items: List, + instruction: InstructionStatus +): List => { + switch (instruction.name) { + case 'createInstructionRelation': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const workspaceId = instruction.accounts.find( + (account) => account.name === 'Workspace' + )?.pubkey; + const applicationId = instruction.accounts.find( + (account) => account.name === 'Application' + )?.pubkey; + const instructionId = instruction.accounts.find( + (account) => account.name === 'Instruction' + )?.pubkey; + const fromId = instruction.accounts.find( + (account) => account.name === 'From' + )?.pubkey; + const toId = instruction.accounts.find( + (account) => account.name === 'To' + )?.pubkey; + const relationId = instruction.accounts.find( + (account) => account.name === 'Relation' + )?.pubkey; + + if ( + workspaceId === undefined || + applicationId === undefined || + instructionId === undefined || + fromId === undefined || + toId === undefined || + relationId === undefined + ) { + throw new Error('Malformed Create Instruction Relation'); + } + + const itemIndex = items.findIndex((item) => item.id === relationId); + + if (itemIndex === -1) { + return items.push({ + id: relationId, + isCreating: true, + isUpdating: false, + isDeleting: false, + instructionId, + applicationId, + workspaceId, + from: fromId, + to: toId, + }); + } else { + return items.update(itemIndex, (item) => ({ + ...item, + id: relationId, + isCreating: true, + isUpdating: false, + isDeleting: false, + instructionId, + applicationId, + workspaceId, + from: fromId, + to: toId, + })); + } + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Relation' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isCreating: false, + }; + }); + } else { + return items; + } + } + case 'deleteInstructionRelation': + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Relation' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isDeleting: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.filter( + (item) => + !instruction.accounts.some( + (account) => + account.name === 'Relation' && account.pubkey === item.id + ) + ); + } else { + return items; + } + default: + return items; + } +}; diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/remove-by-id.pipe.ts b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/remove-by-id.pipe.ts new file mode 100644 index 00000000..2066edbf --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/remove-by-id.pipe.ts @@ -0,0 +1,15 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { List } from 'immutable'; +import { InstructionAccountItemView } from './types'; + +@Pipe({ + name: 'bdRemoveById', +}) +export class RemoveByIdPipe implements PipeTransform { + transform( + accounts: List, + id: string + ): List { + return accounts.filter((account) => account.id !== id); + } +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/types.ts b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/types.ts new file mode 100644 index 00000000..bb481f28 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/types.ts @@ -0,0 +1,62 @@ +export interface CollectionItemView { + id: string; + name: string; + isCreating: boolean; + isUpdating: boolean; + isDeleting: boolean; + applicationId: string; + workspaceId: string; +} + +export interface InstructionAccountItemView { + id: string; + name: string; + kind: { + id: number; + name: string; + }; + modifier: { + id: number; + name: string; + } | null; + space: number | null; + isCreating: boolean; + isUpdating: boolean; + isDeleting: boolean; + instructionId: string; + applicationId: string; + workspaceId: string; + collection: string | null; + close: string | null; + payer: string | null; +} + +export interface InstructionAccountPayerItemView { + id: string; + payer: string | null; + isUpdating: boolean; +} + +export interface InstructionAccountCollectionItemView { + id: string; + collection: string | null; + isUpdating: boolean; +} + +export interface InstructionAccountCloseItemView { + id: string; + close: string | null; + isUpdating: boolean; +} + +export interface InstructionRelationItemView { + id: string; + isCreating: boolean; + isUpdating: boolean; + isDeleting: boolean; + instructionId: string; + applicationId: string; + workspaceId: string; + to: string; + from: string; +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents-accounts.store.ts b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents-accounts.store.ts new file mode 100644 index 00000000..798222fc --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents-accounts.store.ts @@ -0,0 +1,205 @@ +import { Injectable } from '@angular/core'; +import { InstructionAccountsStore } from '@bulldozer-client/instructions-data-access'; +import { + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; +import { + Document, + flattenInstructions, + InstructionAccount, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined, isTruthy } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-account-instructions'; +import { InstructionAccountItemView } from './types'; + +const documentToView = ( + instructionAccount: Document +): InstructionAccountItemView => { + return { + id: instructionAccount.id, + name: instructionAccount.name, + isCreating: false, + isUpdating: false, + isDeleting: false, + kind: instructionAccount.data.kind, + modifier: instructionAccount.data.modifier, + instructionId: instructionAccount.data.instruction, + applicationId: instructionAccount.data.application, + workspaceId: instructionAccount.data.workspace, + space: instructionAccount.data.space, + close: instructionAccount.data.close, + collection: instructionAccount.data.collection, + payer: instructionAccount.data.payer, + }; +}; + +interface ViewModel { + instructionId: string | null; + transactions: List; +} + +const initialState: ViewModel = { + instructionId: null, + transactions: List(), +}; + +@Injectable() +export class ViewInstructionDocumentsAccountsStore extends ComponentStore { + private readonly _instructionId$ = this.select( + ({ instructionId }) => instructionId + ); + private readonly _topicName$ = this.select( + this._instructionId$.pipe(isNotNullOrUndefined), + (instructionId) => `instructions:${instructionId}:accounts` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly accounts$ = this.select( + this._instructionAccountsStore.instructionAccounts$, + this._instructionStatuses$, + (instructionAccounts, instructionStatuses) => { + if (instructionAccounts === null) { + return null; + } + + return instructionStatuses.reduce( + reduceInstructions, + instructionAccounts.map(documentToView) + ); + }, + { debounce: true } + ); + + constructor( + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _instructionAccountsStore: InstructionAccountsStore + ) { + super(initialState); + + this._instructionAccountsStore.setFilters( + this.select( + this._instructionId$.pipe(isNotNullOrUndefined), + this._hdBroadcasterSocketStore.connected$.pipe(isTruthy), + (instructionId) => ({ instruction: instructionId }) + ) + ); + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) + ) + ); + } + + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) + ); + + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) + ); + + readonly setInstructionId = this.updater( + (state, instructionId) => ({ + ...state, + instructionId, + }) + ); + + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); + } + }) + ); + + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; + }>( + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } + + this.patchState({ transactions: List() }); + + const correlationId = uuid(); + let subscriptionId: string; + + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } + + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName + ); + } + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } + }) + ); + }) + ); +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents-close-references.store.ts b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents-close-references.store.ts new file mode 100644 index 00000000..413c0ac1 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents-close-references.store.ts @@ -0,0 +1,202 @@ +import { Injectable } from '@angular/core'; +import { InstructionAccountClosesStore } from '@bulldozer-client/instructions-data-access'; +import { + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; +import { + Document, + flattenInstructions, + InstructionAccountClose, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-account-close-instructions'; +import { InstructionAccountCloseItemView } from './types'; + +const documentToView = ( + instructionAccountClose: Document +): InstructionAccountCloseItemView => { + return { + id: instructionAccountClose.id, + close: instructionAccountClose.data.close, + isUpdating: false, + }; +}; + +interface ViewModel { + instructionId: string | null; + instructionAccountCloseIds: List | null; + transactions: List; +} + +const initialState: ViewModel = { + instructionId: null, + instructionAccountCloseIds: null, + transactions: List(), +}; + +@Injectable() +export class ViewInstructionDocumentsClosesReferencesStore extends ComponentStore { + private readonly _instructionId$ = this.select( + ({ instructionId }) => instructionId + ); + private readonly _instructionAccountCloseIds$ = this.select( + ({ instructionAccountCloseIds }) => instructionAccountCloseIds + ); + private readonly _topicName$ = this.select( + this._instructionId$.pipe(isNotNullOrUndefined), + (instructionId) => `instructions:${instructionId}:accounts` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly accounts$ = this.select( + this._instructionAccountClosesStore.instructionAccountCloses$, + this._instructionStatuses$, + (instructionAccountCloses, instructionStatuses) => { + if (instructionAccountCloses === null) { + return null; + } + + return instructionStatuses.reduce( + reduceInstructions, + instructionAccountCloses.map(documentToView) + ); + }, + { debounce: true } + ); + + constructor( + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _instructionAccountClosesStore: InstructionAccountClosesStore + ) { + super(initialState); + + this._instructionAccountClosesStore.setInstructionAccountCloseIds( + this._instructionAccountCloseIds$ + ); + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) + ) + ); + } + + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) + ); + + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) + ); + + readonly setInstructionId = this.updater( + (state, instructionId) => ({ + ...state, + instructionId, + }) + ); + + readonly setInstructionAccountCloseIds = this.updater | null>( + (state, instructionAccountCloseIds) => ({ + ...state, + instructionAccountCloseIds, + }) + ); + + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); + } + }) + ); + + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; + }>( + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } + + this.patchState({ transactions: List() }); + + const correlationId = uuid(); + let subscriptionId: string; + + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } + + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName + ); + } + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } + }) + ); + }) + ); +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents-collections-references.store.ts b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents-collections-references.store.ts new file mode 100644 index 00000000..96cd6f3f --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents-collections-references.store.ts @@ -0,0 +1,203 @@ +import { Injectable } from '@angular/core'; +import { InstructionAccountCollectionsStore } from '@bulldozer-client/instructions-data-access'; +import { + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; +import { + Document, + flattenInstructions, + InstructionAccountCollection, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-account-collection-instructions'; +import { InstructionAccountCollectionItemView } from './types'; + +const documentToView = ( + instructionAccountCollection: Document +): InstructionAccountCollectionItemView => { + return { + id: instructionAccountCollection.id, + collection: instructionAccountCollection.data.collection, + isUpdating: false, + }; +}; + +interface ViewModel { + instructionId: string | null; + instructionAccountCollectionIds: List | null; + transactions: List; +} + +const initialState: ViewModel = { + instructionId: null, + instructionAccountCollectionIds: null, + transactions: List(), +}; + +@Injectable() +export class ViewInstructionDocumentsCollectionsReferencesStore extends ComponentStore { + private readonly _instructionId$ = this.select( + ({ instructionId }) => instructionId + ); + private readonly _instructionAccountCollectionIds$ = this.select( + ({ instructionAccountCollectionIds }) => instructionAccountCollectionIds + ); + private readonly _topicName$ = this.select( + this._instructionId$.pipe(isNotNullOrUndefined), + (instructionId) => `instructions:${instructionId}:accounts` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly accounts$ = this.select( + this._instructionAccountCollectionsStore.instructionAccountCollections$, + this._instructionStatuses$, + (instructionAccountCollections, instructionStatuses) => { + if (instructionAccountCollections === null) { + return null; + } + + return instructionStatuses.reduce( + reduceInstructions, + instructionAccountCollections.map(documentToView) + ); + }, + { debounce: true } + ); + + constructor( + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _instructionAccountCollectionsStore: InstructionAccountCollectionsStore + ) { + super(initialState); + + this._instructionAccountCollectionsStore.setInstructionAccountCollectionIds( + this._instructionAccountCollectionIds$ + ); + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) + ) + ); + } + + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) + ); + + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) + ); + + readonly setInstructionId = this.updater( + (state, instructionId) => ({ + ...state, + instructionId, + }) + ); + + readonly setInstructionAccountCollectionIds = + this.updater | null>( + (state, instructionAccountCollectionIds) => ({ + ...state, + instructionAccountCollectionIds, + }) + ); + + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); + } + }) + ); + + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; + }>( + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } + + this.patchState({ transactions: List() }); + + const correlationId = uuid(); + let subscriptionId: string; + + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } + + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName + ); + } + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } + }) + ); + }) + ); +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents-collections.store.ts b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents-collections.store.ts new file mode 100644 index 00000000..50d0010a --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents-collections.store.ts @@ -0,0 +1,191 @@ +import { Injectable } from '@angular/core'; +import { CollectionsStore } from '@bulldozer-client/collections-data-access'; +import { + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; +import { + Collection, + Document, + flattenInstructions, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined, isTruthy } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-collection-instructions'; +import { CollectionItemView } from './types'; + +const documentToView = (document: Document): CollectionItemView => { + return { + id: document.id, + name: document.name, + isCreating: false, + isUpdating: false, + isDeleting: false, + applicationId: document.data.application, + workspaceId: document.data.workspace, + }; +}; + +interface ViewModel { + applicationId: string | null; + transactions: List; +} + +const initialState: ViewModel = { + applicationId: null, + transactions: List(), +}; + +@Injectable() +export class ViewInstructionDocumentsCollectionsStore extends ComponentStore { + readonly applicationId$ = this.select(({ applicationId }) => applicationId); + private readonly _topicName$ = this.select( + this.applicationId$.pipe(isNotNullOrUndefined), + (applicationId) => `applications:${applicationId}:collections` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly collections$ = this.select( + this._collectionsStore.collections$, + this._instructionStatuses$, + (collections, instructionStatuses) => { + if (collections === null) { + return null; + } + + return instructionStatuses.reduce( + reduceInstructions, + collections.map(documentToView) + ); + }, + { debounce: true } + ); + + constructor( + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _collectionsStore: CollectionsStore + ) { + super(initialState); + + this._collectionsStore.setFilters( + this.select( + this.applicationId$.pipe(isNotNullOrUndefined), + this._hdBroadcasterSocketStore.connected$.pipe(isTruthy), + (applicationId) => ({ application: applicationId }) + ) + ); + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) + ) + ); + } + + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) + ); + + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) + ); + + readonly setApplicationId = this.updater( + (state, applicationId) => ({ ...state, applicationId }) + ); + + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); + } + }) + ); + + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; + }>( + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } + + this.patchState({ transactions: List() }); + + const correlationId = uuid(); + let subscriptionId: string; + + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } + + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName + ); + } + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } + }) + ); + }) + ); +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents-payers-references.store.ts b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents-payers-references.store.ts new file mode 100644 index 00000000..08ae1712 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents-payers-references.store.ts @@ -0,0 +1,202 @@ +import { Injectable } from '@angular/core'; +import { InstructionAccountPayersStore } from '@bulldozer-client/instructions-data-access'; +import { + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; +import { + Document, + flattenInstructions, + InstructionAccountPayer, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-account-payer-instructions'; +import { InstructionAccountPayerItemView } from './types'; + +const documentToView = ( + instructionAccountPayer: Document +): InstructionAccountPayerItemView => { + return { + id: instructionAccountPayer.id, + payer: instructionAccountPayer.data.payer, + isUpdating: false, + }; +}; + +interface ViewModel { + instructionId: string | null; + instructionAccountPayerIds: List | null; + transactions: List; +} + +const initialState: ViewModel = { + instructionId: null, + instructionAccountPayerIds: null, + transactions: List(), +}; + +@Injectable() +export class ViewInstructionDocumentsPayersReferencesStore extends ComponentStore { + private readonly _instructionId$ = this.select( + ({ instructionId }) => instructionId + ); + private readonly _instructionAccountPayerIds$ = this.select( + ({ instructionAccountPayerIds }) => instructionAccountPayerIds + ); + private readonly _topicName$ = this.select( + this._instructionId$.pipe(isNotNullOrUndefined), + (instructionId) => `instructions:${instructionId}:accounts` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly accounts$ = this.select( + this._instructionAccountPayersStore.instructionAccountPayers$, + this._instructionStatuses$, + (instructionAccountPayers, instructionStatuses) => { + if (instructionAccountPayers === null) { + return null; + } + + return instructionStatuses.reduce( + reduceInstructions, + instructionAccountPayers.map(documentToView) + ); + }, + { debounce: true } + ); + + constructor( + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _instructionAccountPayersStore: InstructionAccountPayersStore + ) { + super(initialState); + + this._instructionAccountPayersStore.setInstructionAccountPayerIds( + this._instructionAccountPayerIds$ + ); + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) + ) + ); + } + + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) + ); + + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) + ); + + readonly setInstructionId = this.updater( + (state, instructionId) => ({ + ...state, + instructionId, + }) + ); + + readonly setInstructionAccountPayerIds = this.updater | null>( + (state, instructionAccountPayerIds) => ({ + ...state, + instructionAccountPayerIds, + }) + ); + + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); + } + }) + ); + + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; + }>( + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } + + this.patchState({ transactions: List() }); + + const correlationId = uuid(); + let subscriptionId: string; + + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } + + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName + ); + } + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } + }) + ); + }) + ); +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents-relations.store.ts b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents-relations.store.ts new file mode 100644 index 00000000..aee7ba28 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents-relations.store.ts @@ -0,0 +1,200 @@ +import { Injectable } from '@angular/core'; +import { InstructionRelationsStore } from '@bulldozer-client/instructions-data-access'; +import { + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; +import { + flattenInstructions, + InstructionRelation, + InstructionStatus, + Relation, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined, isTruthy } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-relation-instructions'; +import { InstructionRelationItemView } from './types'; + +const documentToView = ( + instructionRelation: Relation +): InstructionRelationItemView => { + return { + id: instructionRelation.id, + isCreating: false, + isUpdating: false, + isDeleting: false, + to: instructionRelation.to, + from: instructionRelation.from, + instructionId: instructionRelation.data.instruction, + applicationId: instructionRelation.data.application, + workspaceId: instructionRelation.data.workspace, + }; +}; + +interface ViewModel { + instructionId: string | null; + transactions: List; +} + +const initialState: ViewModel = { + instructionId: null, + transactions: List(), +}; + +@Injectable() +export class ViewInstructionDocumentsRelationsStore extends ComponentStore { + private readonly _instructionId$ = this.select( + ({ instructionId }) => instructionId + ); + private readonly _topicName$ = this.select( + this._instructionId$.pipe(isNotNullOrUndefined), + (instructionId) => `instructions:${instructionId}:accounts` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly accounts$ = this.select( + this._instructionRelationsStore.instructionRelations$, + this._instructionStatuses$, + (instructionRelations, instructionStatuses) => { + if (instructionRelations === null) { + return null; + } + + return instructionStatuses.reduce( + reduceInstructions, + instructionRelations.map(documentToView) + ); + }, + { debounce: true } + ); + + constructor( + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _instructionRelationsStore: InstructionRelationsStore + ) { + super(initialState); + + this._instructionRelationsStore.setFilters( + this.select( + this._instructionId$.pipe(isNotNullOrUndefined), + this._hdBroadcasterSocketStore.connected$.pipe(isTruthy), + (instructionId) => ({ instruction: instructionId }) + ) + ); + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) + ) + ); + } + + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) + ); + + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) + ); + + readonly setInstructionId = this.updater( + (state, instructionId) => ({ + ...state, + instructionId, + }) + ); + + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); + } + }) + ); + + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; + }>( + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } + + this.patchState({ transactions: List() }); + + const correlationId = uuid(); + let subscriptionId: string; + + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } + + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName + ); + } + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } + }) + ); + }) + ); +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents.component.ts b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents.component.ts new file mode 100644 index 00000000..3f8935cb --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents.component.ts @@ -0,0 +1,704 @@ +import { + ChangeDetectionStrategy, + Component, + HostBinding, + OnInit, +} from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { + CollectionQueryStore, + CollectionsStore, +} from '@bulldozer-client/collections-data-access'; +import { + InstructionAccountApiService, + InstructionAccountClosesStore, + InstructionAccountCollectionsStore, + InstructionAccountPayersStore, + InstructionAccountQueryStore, + InstructionAccountsStore, + InstructionRelationApiService, + InstructionRelationQueryStore, + InstructionRelationsStore, +} from '@bulldozer-client/instructions-data-access'; +import { NotificationStore } from '@bulldozer-client/notifications-data-access'; +import { HdBroadcasterSocketStore } from '@heavy-duty/broadcaster'; +import { InstructionAccountDto } from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { Keypair } from '@solana/web3.js'; +import { distinctUntilChanged, map } from 'rxjs'; +import { ViewInstructionDocumentsAccountsStore } from './view-instruction-documents-accounts.store'; +import { ViewInstructionDocumentsClosesReferencesStore } from './view-instruction-documents-close-references.store'; +import { ViewInstructionDocumentsCollectionsReferencesStore } from './view-instruction-documents-collections-references.store'; +import { ViewInstructionDocumentsCollectionsStore } from './view-instruction-documents-collections.store'; +import { ViewInstructionDocumentsPayersReferencesStore } from './view-instruction-documents-payers-references.store'; +import { ViewInstructionDocumentsRelationsStore } from './view-instruction-documents-relations.store'; +import { ViewInstructionDocumentsStore } from './view-instruction-documents.store'; + +@Component({ + selector: 'bd-view-instruction-documents', + template: ` +
+
+

Documents

+

+ The documents are the entities that make up the instruction. +

+
+ + + + + + + + + + +
+ +
+
+
+
+ +
+
+ +
+ +
+ +

+ + Creating + + + Updating + + + Deleting + +

+
+ +
+
+

+ {{ instructionDocument.name }} +

+ +

+ Collection: + + {{ instructionDocument.collection.name }} + +

+

+ + create + + + update + + + delete + + + + + + readonly + +

+
+ +
+

+ Payed by: + +
+ + {{ instructionDocument.payer?.name }} + +
+ + ({{ instructionDocument.space }} bytes) +

+ +

+ Rent sent to: + +
+ + + {{ instructionDocument.close.name }} + +

+
+
+
+
+ + + + + + + + + + + + + +
+ +
+
+

relations

+ + + + +
+ +
+
+
+
+

+ {{ relation.to.name }} +

+ +

+ ({{ relation.id | obscureAddress }}) +

+
+ + +
+
+
+
+
+
+
+ + +

There's no documents yet.

+
+
+ `, + styles: [], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + InstructionAccountsStore, + InstructionAccountPayersStore, + InstructionAccountClosesStore, + InstructionAccountCollectionsStore, + InstructionAccountQueryStore, + InstructionRelationsStore, + InstructionRelationQueryStore, + CollectionsStore, + CollectionQueryStore, + ViewInstructionDocumentsStore, + ViewInstructionDocumentsCollectionsStore, + ViewInstructionDocumentsAccountsStore, + ViewInstructionDocumentsPayersReferencesStore, + ViewInstructionDocumentsClosesReferencesStore, + ViewInstructionDocumentsCollectionsReferencesStore, + ViewInstructionDocumentsRelationsStore, + ], +}) +export class ViewInstructionDocumentsComponent implements OnInit { + @HostBinding('class') class = 'block p-8 pt-5 h-full'; + instructionBody: string | null = null; + readonly collections$ = + this._viewInstructionDocumentsCollectionsStore.collections$.pipe( + map( + (collections) => + collections?.filter( + (collection) => !collection.isCreating && !collection.isDeleting + ) ?? null + ) + ); + readonly instructionAccounts$ = + this._viewInstructionDocumentsAccountsStore.accounts$.pipe( + map( + (accounts) => + accounts?.filter( + (account) => !account.isCreating && !account.isDeleting + ) ?? null + ) + ); + readonly documents$ = this._viewInstructionDocumentsStore.documents$; + readonly workspaceId$ = this._route.paramMap.pipe( + map((paramMap) => paramMap.get('workspaceId')), + isNotNullOrUndefined, + distinctUntilChanged() + ); + readonly applicationId$ = this._route.paramMap.pipe( + map((paramMap) => paramMap.get('applicationId')), + isNotNullOrUndefined, + distinctUntilChanged() + ); + readonly instructionId$ = this._route.paramMap.pipe( + map((paramMap) => paramMap.get('instructionId')), + isNotNullOrUndefined, + distinctUntilChanged() + ); + + constructor( + private readonly _route: ActivatedRoute, + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _notificationStore: NotificationStore, + private readonly _instructionAccountApiService: InstructionAccountApiService, + private readonly _instructionRelationApiService: InstructionRelationApiService, + private readonly _viewInstructionDocumentsStore: ViewInstructionDocumentsStore, + private readonly _viewInstructionDocumentsAccountsStore: ViewInstructionDocumentsAccountsStore, + private readonly _viewInstructionDocumentsRelationsStore: ViewInstructionDocumentsRelationsStore, + private readonly _viewInstructionDocumentsCollectionsStore: ViewInstructionDocumentsCollectionsStore, + private readonly _viewInstructionDocumentsPayersReferencesStore: ViewInstructionDocumentsPayersReferencesStore, + private readonly _viewInstructionDocumentsCollectionsReferencesStore: ViewInstructionDocumentsCollectionsReferencesStore, + private readonly _viewInstructionDocumentsClosesReferencesStore: ViewInstructionDocumentsClosesReferencesStore + ) {} + + ngOnInit() { + this._viewInstructionDocumentsAccountsStore.setInstructionId( + this.instructionId$ + ); + this._viewInstructionDocumentsRelationsStore.setInstructionId( + this.instructionId$ + ); + this._viewInstructionDocumentsPayersReferencesStore.setInstructionId( + this.instructionId$ + ); + this._viewInstructionDocumentsCollectionsReferencesStore.setInstructionId( + this.instructionId$ + ); + this._viewInstructionDocumentsClosesReferencesStore.setInstructionId( + this.instructionId$ + ); + this._viewInstructionDocumentsCollectionsStore.setApplicationId( + this.applicationId$ + ); + } + + onCreateInstructionDocument( + authority: string, + workspaceId: string, + applicationId: string, + instructionId: string, + instructionAccountDto: InstructionAccountDto + ) { + const instructionAccountKeypair = Keypair.generate(); + + this._instructionAccountApiService + .create(instructionAccountKeypair, { + instructionAccountDto, + authority, + workspaceId, + applicationId, + instructionId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Create document request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `instructions:${instructionId}:accounts`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); + } + + onUpdateInstructionDocument( + authority: string, + workspaceId: string, + applicationId: string, + instructionId: string, + instructionAccountId: string, + instructionAccountDto: InstructionAccountDto + ) { + this._instructionAccountApiService + .update({ + authority, + workspaceId, + applicationId, + instructionId, + instructionAccountDto, + instructionAccountId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Update document request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `instructions:${instructionId}:accounts`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); + } + + onDeleteInstructionDocument( + authority: string, + workspaceId: string, + instructionId: string, + instructionAccountId: string + ) { + this._instructionAccountApiService + .delete({ + authority, + workspaceId, + instructionAccountId, + instructionId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Delete document request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `instructions:${instructionId}:accounts`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); + } + + onCreateInstructionRelation( + authority: string, + workspaceId: string, + applicationId: string, + instructionId: string, + fromAccountId: string, + toAccountId: string + ) { + this._instructionRelationApiService + .create({ + fromAccountId, + toAccountId, + authority, + workspaceId, + applicationId, + instructionId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Create relation request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `instructions:${instructionId}:accounts`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); + } + + onDeleteInstructionRelation( + authority: string, + workspaceId: string, + instructionId: string, + fromAccountId: string, + toAccountId: string + ) { + this._instructionRelationApiService + .delete({ + authority, + workspaceId, + instructionId, + fromAccountId, + toAccountId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Delete relation request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `instructions:${instructionId}:accounts`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); + } +} diff --git a/libs/bulldozer/client/instructions/components/instruction-documents-list/src/lib/instruction-documents-list.module.ts b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents.module.ts similarity index 68% rename from libs/bulldozer/client/instructions/components/instruction-documents-list/src/lib/instruction-documents-list.module.ts rename to libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents.module.ts index 91821bb2..61a3b34e 100644 --- a/libs/bulldozer/client/instructions/components/instruction-documents-list/src/lib/instruction-documents-list.module.ts +++ b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents.module.ts @@ -8,19 +8,26 @@ import { MatMenuModule } from '@angular/material/menu'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; +import { CardModule } from '@bulldozer-client/bd-card'; import { EditInstructionDocumentModule } from '@bulldozer-client/edit-instruction-document'; import { EditInstructionRelationModule } from '@bulldozer-client/edit-instruction-relation'; import { ItemUpdatingModule } from '@bulldozer-client/item-updating'; import { ObscureAddressModule } from '@bulldozer-client/obscure-address'; -import { SectionHeaderModule } from '@bulldozer-client/section-header'; +import { HdWalletAdapterCdkModule } from '@heavy-duty/wallet-adapter-cdk'; import { ReactiveComponentModule } from '@ngrx/component'; -import { InstructionDocumentsListComponent } from './instruction-documents-list.component'; +import { RemoveByIdPipe } from './remove-by-id.pipe'; +import { ViewInstructionDocumentsComponent } from './view-instruction-documents.component'; @NgModule({ - declarations: [InstructionDocumentsListComponent], imports: [ CommonModule, - RouterModule, + RouterModule.forChild([ + { + path: '', + pathMatch: 'full', + component: ViewInstructionDocumentsComponent, + }, + ]), MatButtonModule, MatCardModule, MatIconModule, @@ -29,12 +36,13 @@ import { InstructionDocumentsListComponent } from './instruction-documents-list. MatProgressSpinnerModule, MatTooltipModule, ReactiveComponentModule, - SectionHeaderModule, - ObscureAddressModule, + HdWalletAdapterCdkModule, + ItemUpdatingModule, EditInstructionDocumentModule, EditInstructionRelationModule, - ItemUpdatingModule, + ObscureAddressModule, + CardModule, ], - exports: [InstructionDocumentsListComponent], + declarations: [RemoveByIdPipe, ViewInstructionDocumentsComponent], }) -export class InstructionDocumentsListModule {} +export class ViewInstructionDocumentsModule {} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents.store.ts b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents.store.ts new file mode 100644 index 00000000..72cd1554 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/lib/view-instruction-documents.store.ts @@ -0,0 +1,139 @@ +import { Injectable } from '@angular/core'; +import { InstructionAccountsStore } from '@bulldozer-client/instructions-data-access'; +import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { map } from 'rxjs'; +import { ViewInstructionDocumentsAccountsStore } from './view-instruction-documents-accounts.store'; +import { ViewInstructionDocumentsClosesReferencesStore } from './view-instruction-documents-close-references.store'; +import { ViewInstructionDocumentsCollectionsReferencesStore } from './view-instruction-documents-collections-references.store'; +import { ViewInstructionDocumentsCollectionsStore } from './view-instruction-documents-collections.store'; +import { ViewInstructionDocumentsPayersReferencesStore } from './view-instruction-documents-payers-references.store'; +import { ViewInstructionDocumentsRelationsStore } from './view-instruction-documents-relations.store'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface ViewModel {} + +const initialState: ViewModel = {}; + +@Injectable() +export class ViewInstructionDocumentsStore extends ComponentStore { + readonly documents$ = this.select( + this._viewInstructionDocumentsAccountsStore.accounts$, + this._viewInstructionDocumentsCollectionsStore.collections$, + this._viewInstructionDocumentsPayersReferencesStore.accounts$, + this._viewInstructionDocumentsCollectionsReferencesStore.accounts$, + this._viewInstructionDocumentsClosesReferencesStore.accounts$, + this._viewInstructionDocumentsRelationsStore.accounts$, + ( + instructionAccounts, + collections, + payers, + collectionsReferences, + closes, + relations + ) => { + if ( + instructionAccounts === null || + collections === null || + payers === null || + collectionsReferences === null || + closes === null || + relations === null + ) { + return null; + } + + return instructionAccounts + .filter((instructionAccount) => instructionAccount.kind.id === 0) + .map((instructionAccount) => { + const payerAccountId = + payers.find((payer) => payer.id === instructionAccount.payer) + ?.payer ?? null; + const closeAccountId = + closes.find((close) => close.id === instructionAccount.close) + ?.close ?? null; + const collectionId = + collectionsReferences.find( + (collection) => collection.id === instructionAccount.collection + )?.collection ?? null; + + return { + ...instructionAccount, + payer: + instructionAccounts.find( + (instructionAccount) => payerAccountId === instructionAccount.id + ) ?? null, + close: + instructionAccounts.find( + (instructionAccount) => closeAccountId === instructionAccount.id + ) ?? null, + collection: + collections.find( + (collection) => collectionId === collection.id + ) ?? null, + relations: relations + .filter((relation) => relation.from === instructionAccount.id) + .map((relation) => { + return { + ...relation, + to: + instructionAccounts.find( + (instructionAccount) => + relation.to === instructionAccount.id + ) ?? null, + }; + }), + }; + }); + } + ); + + constructor( + private readonly _viewInstructionDocumentsCollectionsStore: ViewInstructionDocumentsCollectionsStore, + private readonly _viewInstructionDocumentsAccountsStore: ViewInstructionDocumentsAccountsStore, + private readonly _viewInstructionDocumentsRelationsStore: ViewInstructionDocumentsRelationsStore, + private readonly _instructionAccountsStore: InstructionAccountsStore, + private readonly _viewInstructionDocumentsPayersReferencesStore: ViewInstructionDocumentsPayersReferencesStore, + private readonly _viewInstructionDocumentsCollectionsReferencesStore: ViewInstructionDocumentsCollectionsReferencesStore, + private readonly _viewInstructionDocumentsClosesReferencesStore: ViewInstructionDocumentsClosesReferencesStore + ) { + super(initialState); + + this._viewInstructionDocumentsPayersReferencesStore.setInstructionAccountPayerIds( + this._instructionAccountsStore.instructionAccounts$.pipe( + isNotNullOrUndefined, + map((accounts) => + accounts + .filter((account) => account.data.kind.id === 0) + .map((account) => account.data.payer) + .filter((payer): payer is string => payer !== null) + .toList() + ) + ) + ); + this._viewInstructionDocumentsCollectionsReferencesStore.setInstructionAccountCollectionIds( + this._instructionAccountsStore.instructionAccounts$.pipe( + isNotNullOrUndefined, + map((accounts) => + accounts + .filter((account) => account.data.kind.id === 0) + .map((account) => account.data.collection) + .filter((collection): collection is string => collection !== null) + .toList() + ) + ) + ); + this._viewInstructionDocumentsClosesReferencesStore.setInstructionAccountCloseIds( + this._instructionAccountsStore.instructionAccounts$.pipe( + isNotNullOrUndefined, + map((accounts) => + accounts + .filter((account) => account.data.kind.id === 0) + .map((account) => account.data.close) + .filter((close): close is string => close !== null) + .toList() + ) + ) + ); + } +} diff --git a/libs/bulldozer/client/instructions/components/instruction-tab/src/test-setup.ts b/libs/bulldozer/client/instructions/pages/view-instruction-documents/src/test-setup.ts similarity index 100% rename from libs/bulldozer/client/instructions/components/instruction-tab/src/test-setup.ts rename to libs/bulldozer/client/instructions/pages/view-instruction-documents/src/test-setup.ts diff --git a/libs/bulldozer/client/instructions/components/instruction-signers-list/tsconfig.json b/libs/bulldozer/client/instructions/pages/view-instruction-documents/tsconfig.json similarity index 100% rename from libs/bulldozer/client/instructions/components/instruction-signers-list/tsconfig.json rename to libs/bulldozer/client/instructions/pages/view-instruction-documents/tsconfig.json diff --git a/libs/bulldozer/client/instructions/components/instruction-signers-list/tsconfig.lib.json b/libs/bulldozer/client/instructions/pages/view-instruction-documents/tsconfig.lib.json similarity index 100% rename from libs/bulldozer/client/instructions/components/instruction-signers-list/tsconfig.lib.json rename to libs/bulldozer/client/instructions/pages/view-instruction-documents/tsconfig.lib.json diff --git a/libs/bulldozer/client/instructions/components/instruction-signers-list/tsconfig.spec.json b/libs/bulldozer/client/instructions/pages/view-instruction-documents/tsconfig.spec.json similarity index 100% rename from libs/bulldozer/client/instructions/components/instruction-signers-list/tsconfig.spec.json rename to libs/bulldozer/client/instructions/pages/view-instruction-documents/tsconfig.spec.json diff --git a/libs/bulldozer/client/instructions/components/instruction-tab/.eslintrc.json b/libs/bulldozer/client/instructions/pages/view-instruction-signers/.eslintrc.json similarity index 100% rename from libs/bulldozer/client/instructions/components/instruction-tab/.eslintrc.json rename to libs/bulldozer/client/instructions/pages/view-instruction-signers/.eslintrc.json diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-signers/README.md b/libs/bulldozer/client/instructions/pages/view-instruction-signers/README.md new file mode 100644 index 00000000..549d1df4 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-signers/README.md @@ -0,0 +1,7 @@ +# bulldozer-client-instructions-pages-view-instruction-signers + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test bulldozer-client-instructions-pages-view-instruction-signers` to execute the unit tests. diff --git a/libs/bulldozer/client/instructions/components/instruction-tab/jest.config.js b/libs/bulldozer/client/instructions/pages/view-instruction-signers/jest.config.js similarity index 85% rename from libs/bulldozer/client/instructions/components/instruction-tab/jest.config.js rename to libs/bulldozer/client/instructions/pages/view-instruction-signers/jest.config.js index 84c3c0be..db095474 100644 --- a/libs/bulldozer/client/instructions/components/instruction-tab/jest.config.js +++ b/libs/bulldozer/client/instructions/pages/view-instruction-signers/jest.config.js @@ -1,5 +1,5 @@ module.exports = { - displayName: 'bulldozer-client-instructions-components-instruction-tab', + displayName: 'bulldozer-client-instructions-pages-view-instruction-signers', preset: '../../../../../../jest.preset.js', setupFilesAfterEnv: ['/src/test-setup.ts'], globals: { @@ -9,7 +9,7 @@ module.exports = { }, }, coverageDirectory: - '../../../../../../coverage/libs/bulldozer/client/instructions/components/instruction-tab', + '../../../../../../coverage/libs/bulldozer/client/instructions/pages/view-instruction-signers', transform: { '^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular', }, diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-signers/project.json b/libs/bulldozer/client/instructions/pages/view-instruction-signers/project.json new file mode 100644 index 00000000..1b790ec7 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-signers/project.json @@ -0,0 +1,28 @@ +{ + "projectType": "library", + "root": "libs/bulldozer/client/instructions/pages/view-instruction-signers", + "sourceRoot": "libs/bulldozer/client/instructions/pages/view-instruction-signers/src", + "prefix": "bd", + "targets": { + "test": { + "executor": "@nrwl/jest:jest", + "outputs": [ + "coverage/libs/bulldozer/client/instructions/pages/view-instruction-signers" + ], + "options": { + "jestConfig": "libs/bulldozer/client/instructions/pages/view-instruction-signers/jest.config.js", + "passWithNoTests": true + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "libs/bulldozer/client/instructions/pages/view-instruction-signers/src/**/*.ts", + "libs/bulldozer/client/instructions/pages/view-instruction-signers/src/**/*.html" + ] + } + } + }, + "tags": [] +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-signers/src/index.ts b/libs/bulldozer/client/instructions/pages/view-instruction-signers/src/index.ts new file mode 100644 index 00000000..d9459316 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-signers/src/index.ts @@ -0,0 +1 @@ +export * from './lib/view-instruction-signers.module'; diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-signers/src/lib/reduce-instructions.ts b/libs/bulldozer/client/instructions/pages/view-instruction-signers/src/lib/reduce-instructions.ts new file mode 100644 index 00000000..717c493a --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-signers/src/lib/reduce-instructions.ts @@ -0,0 +1,232 @@ +import { + InstructionAccountDto, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { List } from 'immutable'; +import { InstructionAccountItemView } from './types'; + +const decodeAccountKind = (id: number): { id: number; name: string } => { + switch (id) { + case 0: { + return { + id, + name: 'document', + }; + } + case 1: + return { + id, + name: 'signer', + }; + default: + throw Error('Invalid kind id'); + } +}; + +const decodeAccountModifier = ( + id: number +): { + id: number; + name: string; +} => { + switch (id) { + case 0: { + return { + id, + name: 'init', + }; + } + case 1: + return { + id, + name: 'mut', + }; + default: + throw Error('Invalid kind id'); + } +}; + +export const reduceInstructions = ( + items: List, + instruction: InstructionStatus +): List => { + switch (instruction.name) { + case 'createInstructionAccount': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: InstructionAccountDto; + }; + const name = data.arguments.name; + const kind = decodeAccountKind(data.arguments.kind); + let modifier: { + id: number; + name: string; + } | null = null; + if (data.arguments.modifier !== null) { + modifier = decodeAccountModifier(data.arguments.modifier); + } + + const workspaceId = instruction.accounts.find( + (account) => account.name === 'Workspace' + )?.pubkey; + const applicationId = instruction.accounts.find( + (account) => account.name === 'Application' + )?.pubkey; + const instructionId = instruction.accounts.find( + (account) => account.name === 'Instruction' + )?.pubkey; + const accountId = instruction.accounts.find( + (account) => account.name === 'Account' + )?.pubkey; + + if ( + workspaceId === undefined || + applicationId === undefined || + instructionId === undefined || + accountId === undefined + ) { + throw new Error('Malformed Create Instruction Account'); + } + + const itemIndex = items.findIndex((item) => item.id === accountId); + + if (itemIndex === -1) { + return items.push({ + id: accountId, + name, + kind, + modifier, + isCreating: true, + isUpdating: false, + isDeleting: false, + instructionId, + applicationId, + workspaceId, + }); + } else { + return items.update(itemIndex, (item) => ({ + ...item, + id: accountId, + name, + kind, + modifier, + isCreating: true, + isUpdating: false, + isDeleting: false, + instructionId, + applicationId, + workspaceId, + })); + } + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Account' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isCreating: false, + }; + }); + } else { + return items; + } + } + case 'updateInstructionAccount': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: InstructionAccountDto; + }; + const name = data.arguments.name; + let modifier: { + id: number; + name: string; + } | null = null; + if (data.arguments.modifier !== null) { + modifier = decodeAccountModifier(data.arguments.modifier); + } + + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Account' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + name, + modifier, + isUpdating: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Account' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isUpdating: false, + }; + }); + } else { + return items; + } + } + case 'deleteInstructionAccount': + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + return items.map((item) => { + if ( + !instruction.accounts.some( + (account) => + account.name === 'Account' && account.pubkey === item.id + ) + ) { + return item; + } + + return { + ...item, + isDeleting: true, + }; + }); + } else if (instruction.transactionStatus.status === 'finalized') { + return items.filter( + (item) => + !instruction.accounts.some( + (account) => + account.name === 'Account' && account.pubkey === item.id + ) + ); + } else { + return items; + } + default: + return items; + } +}; diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-signers/src/lib/types.ts b/libs/bulldozer/client/instructions/pages/view-instruction-signers/src/lib/types.ts new file mode 100644 index 00000000..3fced777 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-signers/src/lib/types.ts @@ -0,0 +1,18 @@ +export interface InstructionAccountItemView { + id: string; + name: string; + kind: { + id: number; + name: string; + }; + modifier: { + id: number; + name: string; + } | null; + isCreating: boolean; + isUpdating: boolean; + isDeleting: boolean; + instructionId: string; + applicationId: string; + workspaceId: string; +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-signers/src/lib/view-instruction-signers.component.ts b/libs/bulldozer/client/instructions/pages/view-instruction-signers/src/lib/view-instruction-signers.component.ts new file mode 100644 index 00000000..c353cfec --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-signers/src/lib/view-instruction-signers.component.ts @@ -0,0 +1,350 @@ +import { + ChangeDetectionStrategy, + Component, + HostBinding, + OnInit, +} from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { + InstructionAccountApiService, + InstructionAccountQueryStore, + InstructionAccountsStore, +} from '@bulldozer-client/instructions-data-access'; +import { NotificationStore } from '@bulldozer-client/notifications-data-access'; +import { HdBroadcasterSocketStore } from '@heavy-duty/broadcaster'; +import { InstructionAccountDto } from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { Keypair } from '@solana/web3.js'; +import { distinctUntilChanged, map } from 'rxjs'; +import { ViewInstructionSignersStore } from './view-instruction-signers.store'; + +@Component({ + selector: 'bd-view-instruction-signers', + template: ` +
+
+

Signers

+

+ The signers are all the signatures required to execute the + instruction. +

+
+ + + + + + + + + + +
+ +
+
+
+ +
+ +
+ +
+ + +

+ + Creating + + + Updating + + + Deleting + +

+
+ +
+
+

+ {{ signer.name }} +

+ +

+ + Non-mutable + + + Mutable + +

+
+
+
+ + + + + + + +
+
+ + +

There's no signers yet.

+
+
+ `, + styles: [], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + InstructionAccountsStore, + InstructionAccountQueryStore, + ViewInstructionSignersStore, + ], +}) +export class ViewInstructionSignersComponent implements OnInit { + @HostBinding('class') class = 'block p-8 pt-5 h-full'; + readonly workspaceId$ = this._route.paramMap.pipe( + map((paramMap) => paramMap.get('workspaceId')), + isNotNullOrUndefined, + distinctUntilChanged() + ); + readonly applicationId$ = this._route.paramMap.pipe( + map((paramMap) => paramMap.get('applicationId')), + isNotNullOrUndefined, + distinctUntilChanged() + ); + readonly instructionId$ = this._route.paramMap.pipe( + map((paramMap) => paramMap.get('instructionId')), + isNotNullOrUndefined, + distinctUntilChanged() + ); + readonly signers$ = this._viewInstructionSignersStore.accounts$.pipe( + map( + (accounts) => accounts?.filter((account) => account.kind.id === 1) ?? null + ) + ); + + constructor( + private readonly _route: ActivatedRoute, + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _notificationStore: NotificationStore, + private readonly _instructionAccountApiService: InstructionAccountApiService, + private readonly _viewInstructionSignersStore: ViewInstructionSignersStore + ) {} + + ngOnInit() { + this._viewInstructionSignersStore.setInstructionId(this.instructionId$); + } + + onCreateInstructionAccount( + authority: string, + workspaceId: string, + applicationId: string, + instructionId: string, + instructionAccountDto: InstructionAccountDto + ) { + const instructionaccountKeypair = Keypair.generate(); + + this._instructionAccountApiService + .create(instructionaccountKeypair, { + instructionAccountDto, + authority, + workspaceId, + applicationId, + instructionId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Create signer request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `instructions:${instructionId}:accounts`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); + } + + onUpdateInstructionAccount( + authority: string, + workspaceId: string, + applicationId: string, + instructionId: string, + instructionAccountId: string, + instructionAccountDto: InstructionAccountDto + ) { + this._instructionAccountApiService + .update({ + authority, + workspaceId, + applicationId, + instructionId, + instructionAccountDto, + instructionAccountId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Update signer request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `instructions:${instructionId}:accounts`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); + } + + onDeleteInstructionAccount( + authority: string, + workspaceId: string, + instructionId: string, + instructionAccountId: string + ) { + this._instructionAccountApiService + .delete({ + authority, + workspaceId, + instructionAccountId, + instructionId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Delete signer request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `instructions:${instructionId}:accounts`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); + } +} diff --git a/libs/bulldozer/client/instructions/components/instruction-signers-list/src/lib/instruction-signers-list.module.ts b/libs/bulldozer/client/instructions/pages/view-instruction-signers/src/lib/view-instruction-signers.module.ts similarity index 64% rename from libs/bulldozer/client/instructions/components/instruction-signers-list/src/lib/instruction-signers-list.module.ts rename to libs/bulldozer/client/instructions/pages/view-instruction-signers/src/lib/view-instruction-signers.module.ts index 3fa4ce9b..1140ca80 100644 --- a/libs/bulldozer/client/instructions/components/instruction-signers-list/src/lib/instruction-signers-list.module.ts +++ b/libs/bulldozer/client/instructions/pages/view-instruction-signers/src/lib/view-instruction-signers.module.ts @@ -7,16 +7,24 @@ import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { RouterModule } from '@angular/router'; +import { CardModule } from '@bulldozer-client/bd-card'; import { EditInstructionSignerModule } from '@bulldozer-client/edit-instruction-signer'; import { ItemUpdatingModule } from '@bulldozer-client/item-updating'; -import { SectionHeaderModule } from '@bulldozer-client/section-header'; +import { HdWalletAdapterCdkModule } from '@heavy-duty/wallet-adapter-cdk'; import { ReactiveComponentModule } from '@ngrx/component'; -import { InstructionSignersListComponent } from './instruction-signers-list.component'; +import { ViewInstructionSignersComponent } from './view-instruction-signers.component'; @NgModule({ - declarations: [InstructionSignersListComponent], imports: [ CommonModule, + RouterModule.forChild([ + { + path: '', + pathMatch: 'full', + component: ViewInstructionSignersComponent, + }, + ]), MatButtonModule, MatCardModule, MatIconModule, @@ -25,10 +33,11 @@ import { InstructionSignersListComponent } from './instruction-signers-list.comp MatProgressSpinnerModule, MatTooltipModule, ReactiveComponentModule, - SectionHeaderModule, - EditInstructionSignerModule, + HdWalletAdapterCdkModule, ItemUpdatingModule, + EditInstructionSignerModule, + CardModule, ], - exports: [InstructionSignersListComponent], + declarations: [ViewInstructionSignersComponent], }) -export class InstructionSignersListModule {} +export class ViewInstructionSignersModule {} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction-signers/src/lib/view-instruction-signers.store.ts b/libs/bulldozer/client/instructions/pages/view-instruction-signers/src/lib/view-instruction-signers.store.ts new file mode 100644 index 00000000..4e5f0d68 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction-signers/src/lib/view-instruction-signers.store.ts @@ -0,0 +1,212 @@ +import { Injectable } from '@angular/core'; +import { InstructionAccountsStore } from '@bulldozer-client/instructions-data-access'; +import { + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; +import { + Document, + flattenInstructions, + InstructionAccount, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined, isTruthy } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-instructions'; +import { InstructionAccountItemView } from './types'; + +const documentToView = ( + document: Document +): InstructionAccountItemView => { + return { + id: document.id, + name: document.name, + isCreating: false, + isUpdating: false, + isDeleting: false, + kind: { + id: 1, + name: 'signer', + }, + modifier: + document.data.modifier !== null + ? { + id: document.data.modifier.id, + name: document.data.modifier.name, + } + : null, + instructionId: document.data.instruction, + applicationId: document.data.application, + workspaceId: document.data.workspace, + }; +}; + +interface ViewModel { + instructionId: string | null; + transactions: List; +} + +const initialState: ViewModel = { + instructionId: null, + transactions: List(), +}; + +@Injectable() +export class ViewInstructionSignersStore extends ComponentStore { + private readonly _instructionId$ = this.select( + ({ instructionId }) => instructionId + ); + private readonly _topicName$ = this.select( + this._instructionId$.pipe(isNotNullOrUndefined), + (instructionId) => `instructions:${instructionId}:accounts` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly accounts$ = this.select( + this._instructionAccountsStore.instructionAccounts$, + this._instructionStatuses$, + (instructionAccounts, instructionStatuses) => { + if (instructionAccounts === null) { + return null; + } + + return instructionStatuses.reduce( + reduceInstructions, + instructionAccounts + .filter((instructionAccount) => instructionAccount.data.kind.id === 1) + .map(documentToView) + ); + }, + { debounce: true } + ); + + constructor( + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _instructionAccountsStore: InstructionAccountsStore + ) { + super(initialState); + + this._instructionAccountsStore.setFilters( + this.select( + this._instructionId$.pipe(isNotNullOrUndefined), + this._hdBroadcasterSocketStore.connected$.pipe(isTruthy), + (instructionId) => ({ instruction: instructionId }) + ) + ); + this._registerTopic( + this.select( + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) + ) + ); + } + + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) + ); + + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) + ); + + readonly setInstructionId = this.updater( + (state, instructionId) => ({ + ...state, + instructionId, + }) + ); + + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); + } + }) + ); + + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; + }>( + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } + + this.patchState({ transactions: List() }); + + const correlationId = uuid(); + let subscriptionId: string; + + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } + + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName + ); + } + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } + }) + ); + }) + ); +} diff --git a/libs/bulldozer/client/users/components/profile-tab/src/test-setup.ts b/libs/bulldozer/client/instructions/pages/view-instruction-signers/src/test-setup.ts similarity index 100% rename from libs/bulldozer/client/users/components/profile-tab/src/test-setup.ts rename to libs/bulldozer/client/instructions/pages/view-instruction-signers/src/test-setup.ts diff --git a/libs/bulldozer/client/instructions/components/instruction-tab/tsconfig.json b/libs/bulldozer/client/instructions/pages/view-instruction-signers/tsconfig.json similarity index 100% rename from libs/bulldozer/client/instructions/components/instruction-tab/tsconfig.json rename to libs/bulldozer/client/instructions/pages/view-instruction-signers/tsconfig.json diff --git a/libs/bulldozer/client/instructions/components/instruction-tab/tsconfig.lib.json b/libs/bulldozer/client/instructions/pages/view-instruction-signers/tsconfig.lib.json similarity index 100% rename from libs/bulldozer/client/instructions/components/instruction-tab/tsconfig.lib.json rename to libs/bulldozer/client/instructions/pages/view-instruction-signers/tsconfig.lib.json diff --git a/libs/bulldozer/client/instructions/components/instruction-tab/tsconfig.spec.json b/libs/bulldozer/client/instructions/pages/view-instruction-signers/tsconfig.spec.json similarity index 100% rename from libs/bulldozer/client/instructions/components/instruction-tab/tsconfig.spec.json rename to libs/bulldozer/client/instructions/pages/view-instruction-signers/tsconfig.spec.json diff --git a/libs/bulldozer/client/instructions/pages/view-instruction/src/index.ts b/libs/bulldozer/client/instructions/pages/view-instruction/src/index.ts index 7104e485..e1a3d5db 100644 --- a/libs/bulldozer/client/instructions/pages/view-instruction/src/index.ts +++ b/libs/bulldozer/client/instructions/pages/view-instruction/src/index.ts @@ -1 +1,2 @@ +export * from './lib/view-instruction-tab.module'; export * from './lib/view-instruction.module'; diff --git a/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/reduce-instructions.ts b/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/reduce-instructions.ts new file mode 100644 index 00000000..af21ce3d --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/reduce-instructions.ts @@ -0,0 +1,107 @@ +import { + InstructionDto, + InstructionStatus, +} from '@heavy-duty/bulldozer-devkit'; +import { InstructionItemView } from './types'; + +export const reduceInstructions = ( + item: InstructionItemView | null, + instruction: InstructionStatus +): InstructionItemView | null => { + switch (instruction.name) { + case 'createInstruction': { + if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: InstructionDto; + }; + const name = data.arguments.name; + + const workspaceId = instruction.accounts.find( + (account) => account.name === 'Workspace' + )?.pubkey; + const applicationId = instruction.accounts.find( + (account) => account.name === 'Application' + )?.pubkey; + const instructionId = instruction.accounts.find( + (account) => account.name === 'Instruction' + )?.pubkey; + + if ( + workspaceId === undefined || + applicationId === undefined || + instructionId === undefined + ) { + throw new Error('Malformed Create Instruction'); + } + + return { + id: instructionId, + name, + isCreating: true, + isUpdating: false, + isDeleting: false, + applicationId, + workspaceId, + }; + } else if (instruction.transactionStatus.status === 'finalized') { + if (item === null) { + return null; + } else { + return { + ...item, + isCreating: false, + }; + } + } else { + return item; + } + } + case 'updateInstruction': { + if (item === null) { + return item; + } else if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + const data = instruction.data as { + arguments: InstructionDto; + }; + const name = data.arguments.name; + + return { + ...item, + name, + isUpdating: true, + }; + } else if (instruction.transactionStatus.status === 'finalized') { + return { + ...item, + isUpdating: false, + }; + } else { + return item; + } + } + case 'deleteInstruction': + if (item === null) { + return item; + } else if ( + instruction.transactionStatus.status === undefined || + instruction.transactionStatus.status === 'confirmed' + ) { + return { + ...item, + isDeleting: true, + }; + } else if (instruction.transactionStatus.status === 'finalized') { + return null; + } else { + return item; + } + default: + return item; + } +}; diff --git a/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/types.ts b/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/types.ts new file mode 100644 index 00000000..548a9a97 --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/types.ts @@ -0,0 +1,9 @@ +export interface InstructionItemView { + id: string; + name: string; + isCreating: boolean; + isUpdating: boolean; + isDeleting: boolean; + applicationId: string; + workspaceId: string; +} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction-code.store.ts b/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction-code.store.ts deleted file mode 100644 index b84ec9c6..00000000 --- a/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction-code.store.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { Injectable } from '@angular/core'; -import { CodeEditorOptions } from '@bulldozer-client/code-editor'; -import { CollectionsStore } from '@bulldozer-client/collections-data-access'; -import { DarkThemeStore } from '@bulldozer-client/core-data-access'; -import { - InstructionAccountsStore, - InstructionArgumentsStore, - InstructionRelationsStore, - InstructionStore, -} from '@bulldozer-client/instructions-data-access'; -import { - Collection, - Document, - Instruction, - InstructionAccount, - InstructionArgument, - InstructionRelation, - Relation, -} from '@heavy-duty/bulldozer-devkit'; -import { generateInstructionCode } from '@heavy-duty/generator'; -import { ComponentStore } from '@ngrx/component-store'; - -const COMMON_EDITOR_OPTIONS: CodeEditorOptions = { - language: 'rust', - automaticLayout: true, - fontSize: 16, - wordWrap: 'on', - theme: 'vs-light', - readOnly: false, -}; - -interface ViewModel { - contextCode: string | null; - contextEditorOptions: CodeEditorOptions; - handleCode: string | null; - handleEditorOptions: CodeEditorOptions; -} - -const initialState: ViewModel = { - contextCode: null, - contextEditorOptions: { - ...COMMON_EDITOR_OPTIONS, - readOnly: true, - }, - handleCode: null, - handleEditorOptions: COMMON_EDITOR_OPTIONS, -}; - -@Injectable() -export class ViewInstructionCodeStore extends ComponentStore { - readonly contextCode$ = this.select(({ contextCode }) => contextCode); - readonly contextEditorOptions$ = this.select( - ({ contextEditorOptions }) => contextEditorOptions - ); - readonly handleCode$ = this.select(({ handleCode }) => handleCode); - readonly handleEditorOptions$ = this.select( - ({ handleEditorOptions }) => handleEditorOptions - ); - - constructor( - private readonly _instructionStore: InstructionStore, - instructionArgumentsStore: InstructionArgumentsStore, - instructionAccountsStore: InstructionAccountsStore, - instructionRelationsStore: InstructionRelationsStore, - collectionsStore: CollectionsStore, - darkThemeStore: DarkThemeStore - ) { - super(initialState); - - this._loadContextCode( - this.select( - this._instructionStore.instruction$, - instructionArgumentsStore.instructionArguments$, - instructionAccountsStore.instructionAccounts$, - instructionRelationsStore.instructionRelations$, - collectionsStore.collections$, - ( - instruction, - instructionArguments, - instructionAccounts, - instructionRelations, - collections - ) => ({ - instruction: instruction?.document ?? null, - instructionArguments: instructionArguments.map( - ({ document }) => document - ), - instructionAccounts: instructionAccounts.map( - ({ document }) => document - ), - instructionRelations: instructionRelations.map( - ({ document }) => document - ), - collections: collections.map(({ document }) => document), - }), - { debounce: true } - ) - ); - this._loadEditorOptions(darkThemeStore.isDarkThemeEnabled$); - this._loadHandleCode( - this.select( - this._instructionStore.instruction$, - (instruction) => instruction?.document.data.body ?? null - ) - ); - } - - private readonly _loadEditorOptions = this.updater( - (state, isDarkThemeEnabled) => ({ - ...state, - contextEditorOptions: { - ...COMMON_EDITOR_OPTIONS, - theme: isDarkThemeEnabled ? 'vs-dark' : 'vs-light', - readOnly: true, - }, - handleEditorOptions: { - ...COMMON_EDITOR_OPTIONS, - theme: isDarkThemeEnabled ? 'vs-dark' : 'vs-light', - readOnly: false, - }, - }) - ); - - private readonly _loadHandleCode = this.updater( - (state, handleCode) => ({ ...state, handleCode }) - ); - - private readonly _loadContextCode = this.updater<{ - instruction: Document | null; - instructionArguments: Document[]; - instructionAccounts: Document[]; - instructionRelations: Relation[]; - collections: Document[]; - }>( - ( - state, - { - instruction, - instructionArguments, - instructionAccounts, - instructionRelations, - collections, - } - ) => ({ - ...state, - contextCode: - instruction && - generateInstructionCode( - instruction, - instructionArguments, - instructionAccounts, - instructionRelations, - collections - ), - }) - ); -} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction-documents.store.ts b/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction-documents.store.ts deleted file mode 100644 index 8412983c..00000000 --- a/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction-documents.store.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Injectable } from '@angular/core'; -import { - CollectionItemView, - CollectionsStore, -} from '@bulldozer-client/collections-data-access'; -import { - InstructionAccountItemView, - InstructionAccountsStore, - InstructionDocumentItemView, - InstructionRelationItemView, - InstructionRelationsStore, -} from '@bulldozer-client/instructions-data-access'; -import { ComponentStore } from '@ngrx/component-store'; -import { Observable } from 'rxjs'; - -interface ViewModel { - instructionId: string | null; -} - -const initialState: ViewModel = { - instructionId: null, -}; - -@Injectable() -export class ViewInstructionDocumentsStore extends ComponentStore { - readonly instructionDocuments$: Observable = - this.select( - this._instructionAccountsStore.instructionAccounts$, - this._collectionsStore.collections$, - this._instructionRelationsStore.instructionRelations$, - this.select(({ instructionId }) => instructionId), - (instructionAccounts, collections, instructionRelations, instructionId) => - instructionAccounts - .filter( - ({ document }) => - document.data.instruction === instructionId && - document.data.kind.id === 0 - ) - .map((instructionAccount) => ({ - isCreating: instructionAccount.isCreating, - isUpdating: instructionAccount.isUpdating, - isDeleting: instructionAccount.isDeleting, - document: instructionAccount.document, - relations: instructionRelations - .filter( - ({ document }) => - document.from === instructionAccount.document.id - ) - .reduce( - ( - relations: (InstructionRelationItemView & { - extras: { to: InstructionAccountItemView }; - })[], - instructionRelation - ) => { - const toAccount = instructionAccounts.find( - ({ document }) => - document.id === instructionRelation.document.to - ); - - return toAccount - ? [ - ...relations, - { - ...instructionRelation, - extras: { to: toAccount }, - }, - ] - : relations; - }, - [] - ), - collection: collections.reduce( - (found: CollectionItemView | null, collection) => - !found && - instructionAccount.document.data.kind.collection === - collection.document.id - ? collection - : null, - null - ), - payer: instructionAccounts.reduce( - (found: InstructionAccountItemView | null, payer) => - !found && - instructionAccount.document.data.modifier?.payer === - payer.document.id - ? payer - : null, - null - ), - close: instructionAccounts.reduce( - (found: InstructionAccountItemView | null, close) => - !found && - instructionAccount.document.data.modifier?.close === - close.document.id - ? close - : null, - null - ), - })) - ); - - constructor( - private readonly _instructionAccountsStore: InstructionAccountsStore, - private readonly _collectionsStore: CollectionsStore, - private readonly _instructionRelationsStore: InstructionRelationsStore - ) { - super(initialState); - } - - readonly setInstructionId = this.updater( - (state, instructionId) => ({ ...state, instructionId }) - ); -} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction-signers.store.ts b/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction-signers.store.ts deleted file mode 100644 index 197c4ee6..00000000 --- a/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction-signers.store.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Injectable } from '@angular/core'; -import { - InstructionAccountItemView, - InstructionAccountsStore, -} from '@bulldozer-client/instructions-data-access'; -import { ComponentStore } from '@ngrx/component-store'; - -interface ViewModel { - instructionSigners: InstructionAccountItemView[]; - instructionId: string | null; -} - -const initialState: ViewModel = { - instructionSigners: [], - instructionId: null, -}; - -@Injectable() -export class ViewInstructionSignersStore extends ComponentStore { - readonly instructionSigners$ = this.select( - this._instructionAccountsStore.instructionAccounts$, - this.select(({ instructionId }) => instructionId), - (instructionAccounts, instructionId) => - instructionAccounts.filter( - ({ document }) => - document.data.instruction === instructionId && - document.data.kind.id === 1 - ) - ); - - constructor( - private readonly _instructionAccountsStore: InstructionAccountsStore - ) { - super(initialState); - } - - readonly setInstructionId = this.updater( - (state, instructionId) => ({ ...state, instructionId }) - ); -} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction-tab.component.ts b/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction-tab.component.ts new file mode 100644 index 00000000..4a8c4bcd --- /dev/null +++ b/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction-tab.component.ts @@ -0,0 +1,84 @@ +import { Component, HostBinding, Input } from '@angular/core'; +import { TabStore } from '@bulldozer-client/core-data-access'; +import { InstructionStore } from '@bulldozer-client/instructions-data-access'; +import { ViewInstructionStore } from './view-instruction.store'; + +@Component({ + selector: 'bd-view-instruction-tab', + template: ` + + `, + providers: [InstructionStore, ViewInstructionStore], +}) +export class ViewInstructionTabComponent { + @HostBinding('class') class = 'block w-full'; + + @Input() url: string | null = null; + @Input() set instructionId(value: string) { + this._viewInstructionStore.setInstructionId(value); + } + + readonly instructionId$ = this._viewInstructionStore.instructionId$; + readonly instruction$ = this._viewInstructionStore.instruction$; + readonly loading$ = this._instructionStore.loading$; + + constructor( + private readonly _tabStore: TabStore, + private readonly _instructionStore: InstructionStore, + private readonly _viewInstructionStore: ViewInstructionStore + ) {} + + onCloseTab(instructionId: string) { + this._tabStore.closeTab(instructionId); + } +} diff --git a/libs/bulldozer/client/workspaces/components/workspace-tab/src/lib/workspace-tab.module.ts b/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction-tab.module.ts similarity index 77% rename from libs/bulldozer/client/workspaces/components/workspace-tab/src/lib/workspace-tab.module.ts rename to libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction-tab.module.ts index b40f513d..78554bbb 100644 --- a/libs/bulldozer/client/workspaces/components/workspace-tab/src/lib/workspace-tab.module.ts +++ b/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction-tab.module.ts @@ -7,20 +7,20 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; import { ItemUpdatingModule } from '@bulldozer-client/item-updating'; import { ReactiveComponentModule } from '@ngrx/component'; -import { WorkspaceTabComponent } from './workspace-tab.component'; +import { ViewInstructionTabComponent } from './view-instruction-tab.component'; @NgModule({ - declarations: [WorkspaceTabComponent], + declarations: [ViewInstructionTabComponent], imports: [ CommonModule, RouterModule, MatButtonModule, MatIconModule, - MatTooltipModule, MatProgressSpinnerModule, + MatTooltipModule, ReactiveComponentModule, ItemUpdatingModule, ], - exports: [WorkspaceTabComponent], + exports: [ViewInstructionTabComponent], }) -export class WorkspaceTabModule {} +export class ViewInstructionTabModule {} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction.component.ts b/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction.component.ts index f60fa31d..c785460e 100644 --- a/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction.component.ts +++ b/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction.component.ts @@ -1,392 +1,396 @@ -import { ChangeDetectionStrategy, Component, HostBinding } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; import { - CollectionQueryStore, - CollectionsStore, -} from '@bulldozer-client/collections-data-access'; + ChangeDetectionStrategy, + Component, + HostBinding, + OnInit, +} from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { TabStore } from '@bulldozer-client/core-data-access'; import { - InstructionAccountQueryStore, - InstructionAccountsStore, - InstructionArgumentQueryStore, - InstructionArgumentsStore, - InstructionRelationQueryStore, - InstructionRelationsStore, + InstructionApiService, InstructionStore, } from '@bulldozer-client/instructions-data-access'; -import { - InstructionAccountDto, - InstructionArgumentDto, -} from '@heavy-duty/bulldozer-devkit'; -import { WalletStore } from '@heavy-duty/wallet-adapter'; -import { map } from 'rxjs'; -import { ViewInstructionCodeStore } from './view-instruction-code.store'; -import { ViewInstructionDocumentsStore } from './view-instruction-documents.store'; -import { ViewInstructionSignersStore } from './view-instruction-signers.store'; +import { NotificationStore } from '@bulldozer-client/notifications-data-access'; +import { HdBroadcasterSocketStore } from '@heavy-duty/broadcaster'; +import { InstructionDto } from '@heavy-duty/bulldozer-devkit'; +import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; +import { combineLatest, distinctUntilChanged, map } from 'rxjs'; import { ViewInstructionStore } from './view-instruction.store'; @Component({ selector: 'bd-view-instruction', template: ` -
-
-
-

- - {{ instruction.document.name }} - - -

-

Visualize all the details about this instruction.

+ +
-
-
- + + + +
+
+
+
+
+
+
+ + + + - -
-

- Remember to save the changes below: - -

-
-
+
+ pipe +
- -
-
+
+
`, styles: [], changeDetection: ChangeDetectionStrategy.OnPush, - providers: [ - InstructionStore, - InstructionArgumentsStore, - InstructionArgumentQueryStore, - InstructionAccountsStore, - InstructionAccountQueryStore, - InstructionRelationsStore, - InstructionRelationQueryStore, - CollectionsStore, - CollectionQueryStore, - ViewInstructionStore, - ViewInstructionDocumentsStore, - ViewInstructionSignersStore, - ViewInstructionCodeStore, - ], + providers: [InstructionStore, ViewInstructionStore], }) -export class ViewInstructionComponent { - @HostBinding('class') class = 'block'; - instructionBody = ''; - readonly connected$ = this._walletStore.connected$; - readonly collections$ = this._collectionsStore.collections$.pipe( - map((collections) => collections.map(({ document }) => document)) +export class ViewInstructionComponent implements OnInit { + @HostBinding('class') class = 'flex h-full'; + readonly instruction$ = this._viewInstructionStore.instruction$; + readonly loading$ = this._instructionStore.loading$; + readonly workspaceId$ = this._route.paramMap.pipe( + map((paramMap) => paramMap.get('workspaceId')), + isNotNullOrUndefined, + distinctUntilChanged() + ); + readonly applicationId$ = this._route.paramMap.pipe( + map((paramMap) => paramMap.get('applicationId')), + isNotNullOrUndefined, + distinctUntilChanged() + ); + readonly instructionId$ = this._route.paramMap.pipe( + map((paramMap) => paramMap.get('instructionId')), + isNotNullOrUndefined, + distinctUntilChanged() ); - readonly instruction$ = this._instructionStore.instruction$; - readonly instructionArguments$ = - this._instructionArgumentsStore.instructionArguments$; - readonly instructionAccounts$ = - this._instructionAccountsStore.instructionAccounts$; - readonly instructionDocuments$ = - this._viewInstructionDocumentsStore.instructionDocuments$; - readonly instructionSigners$ = - this._viewInstructionSignersStore.instructionSigners$; - readonly contextCode$ = this._viewInstructionCodeStore.contextCode$; - readonly contextEditorOptions$ = - this._viewInstructionCodeStore.contextEditorOptions$; - readonly handleEditorOptions$ = - this._viewInstructionCodeStore.handleEditorOptions$; - readonly handleCode$ = this._viewInstructionCodeStore.handleCode$; constructor( + private readonly _router: Router, private readonly _route: ActivatedRoute, + private readonly _tabStore: TabStore, + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _notificationStore: NotificationStore, + private readonly _instructionApiService: InstructionApiService, private readonly _instructionStore: InstructionStore, - private readonly _instructionArgumentsStore: InstructionArgumentsStore, - private readonly _instructionAccountsStore: InstructionAccountsStore, - private readonly _collectionsStore: CollectionsStore, - private readonly _walletStore: WalletStore, - private readonly _viewInstructionStore: ViewInstructionStore, - private readonly _viewInstructionCodeStore: ViewInstructionCodeStore, - private readonly _viewInstructionDocumentsStore: ViewInstructionDocumentsStore, - private readonly _viewInstructionSignersStore: ViewInstructionSignersStore - ) { - this._viewInstructionStore.setWorkspaceId( - this._route.paramMap.pipe(map((paramMap) => paramMap.get('workspaceId'))) - ); - this._viewInstructionStore.setApplicationId( - this._route.paramMap.pipe( - map((paramMap) => paramMap.get('applicationId')) - ) - ); - this._viewInstructionStore.setInstructionId( - this._route.paramMap.pipe( - map((paramMap) => paramMap.get('instructionId')) + private readonly _viewInstructionStore: ViewInstructionStore + ) {} + + ngOnInit() { + this._viewInstructionStore.setInstructionId(this.instructionId$); + this._tabStore.openTab( + combineLatest({ + workspaceId: this.workspaceId$, + applicationId: this.applicationId$, + instructionId: this.instructionId$, + }).pipe( + map(({ instructionId, applicationId, workspaceId }) => ({ + id: instructionId, + kind: 'instruction', + url: `/workspaces/${workspaceId}/applications/${applicationId}/instructions/${instructionId}`, + })) ) ); } - onUpdateInstructionBody( - workspaceId: string, - applicationId: string, - instructionId: string - ) { - this._viewInstructionStore.updateInstructionBody({ - workspaceId, - applicationId, - instructionId, - instructionBody: this.instructionBody, - }); - } - - onCreateInstructionArgument( - workspaceId: string, - applicationId: string, - instructionId: string, - instructionArgumentDto: InstructionArgumentDto - ) { - this._viewInstructionStore.createInstructionArgument({ - workspaceId, - applicationId, - instructionId, - instructionArgumentDto, - }); - } - - onUpdateInstructionArgument( - workspaceId: string, - instructionId: string, - instructionArgumentId: string, - instructionArgumentDto: InstructionArgumentDto - ) { - this._viewInstructionStore.updateInstructionArgument({ - workspaceId, - instructionId, - instructionArgumentId, - instructionArgumentDto, - }); - } - - onDeleteInstructionArgument( - workspaceId: string, - instructionId: string, - instructionArgumentId: string - ) { - this._viewInstructionStore.deleteInstructionArgument({ - workspaceId, - instructionId, - instructionArgumentId, + isRouteActive(url: string) { + return this._router.isActive(url, { + paths: 'exact', + queryParams: 'exact', + fragment: 'ignored', + matrixParams: 'ignored', }); } - onCreateInstructionAccount( + onUpdateInstruction( + authority: string, workspaceId: string, applicationId: string, instructionId: string, - instructionAccountDto: InstructionAccountDto + instructionDto: InstructionDto ) { - this._viewInstructionStore.createInstructionAccount({ - workspaceId, - applicationId, - instructionId, - instructionAccountDto, - }); + this._instructionApiService + .update({ + authority, + workspaceId, + applicationId, + instructionDto, + instructionId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Update instruction request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `applications:${applicationId}:instructions`, + `instructions:${instructionId}`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); } - onUpdateInstructionAccount( - workspaceId: string, - instructionId: string, - instructionAccountId: string, - instructionAccountDto: InstructionAccountDto - ) { - this._viewInstructionStore.updateInstructionAccount({ - workspaceId, - instructionId, - instructionAccountId, - instructionAccountDto, - }); - } - - onDeleteInstructionAccount( - workspaceId: string, - instructionId: string, - instructionAccountId: string - ) { - this._viewInstructionStore.deleteInstructionAccount({ - workspaceId, - instructionId, - instructionAccountId, - }); - } - - onCreateInstructionRelation( + onDeleteInstruction( + authority: string, workspaceId: string, applicationId: string, - instructionId: string, - fromAccountId: string, - toAccountId: string - ) { - this._viewInstructionStore.createInstructionRelation({ - workspaceId, - applicationId, - instructionId, - fromAccountId, - toAccountId, - }); - } - - onDeleteInstructionRelation( - workspaceId: string, - instructionId: string, - fromAccountId: string, - toAccountId: string + instructionId: string ) { - this._viewInstructionStore.deleteInstructionRelation({ - workspaceId, - instructionId, - fromAccountId, - toAccountId, - }); + this._instructionApiService + .delete({ + authority, + workspaceId, + applicationId, + instructionId, + }) + .subscribe({ + next: ({ transactionSignature, transaction }) => { + this._notificationStore.setEvent('Delete instruction request sent'); + this._hdBroadcasterSocketStore.send( + JSON.stringify({ + event: 'transaction', + data: { + transactionSignature, + transaction, + topicNames: [ + `authority:${authority}`, + `applications:${applicationId}:instructions`, + `instructions:${instructionId}`, + ], + }, + }) + ); + }, + error: (error) => { + this._notificationStore.setError(error); + }, + }); } } diff --git a/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction.module.ts b/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction.module.ts index 724729e5..c6518438 100644 --- a/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction.module.ts +++ b/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction.module.ts @@ -5,12 +5,10 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; -import { CodeEditorModule } from '@bulldozer-client/code-editor'; -import { InstructionArgumentsListModule } from '@bulldozer-client/instruction-arguments-list'; -import { InstructionDocumentsListModule } from '@bulldozer-client/instruction-documents-list'; -import { InstructionSignersListModule } from '@bulldozer-client/instruction-signers-list'; +import { EditInstructionModule } from '@bulldozer-client/edit-instruction'; import { ItemUpdatingModule } from '@bulldozer-client/item-updating'; -import { PageHeaderModule } from '@bulldozer-client/page-header'; +import { HdBroadcasterCdkModule } from '@heavy-duty/broadcaster-cdk'; +import { HdWalletAdapterCdkModule } from '@heavy-duty/wallet-adapter-cdk'; import { ReactiveComponentModule } from '@ngrx/component'; import { ViewInstructionComponent } from './view-instruction.component'; @@ -19,19 +17,54 @@ import { ViewInstructionComponent } from './view-instruction.component'; imports: [ CommonModule, RouterModule.forChild([ - { path: '', pathMatch: 'full', component: ViewInstructionComponent }, + { + path: '', + component: ViewInstructionComponent, + children: [ + { + path: 'arguments', + loadChildren: () => + import('@bulldozer-client/view-instruction-arguments').then( + (m) => m.ViewInstructionArgumentsModule + ), + }, + { + path: 'documents', + loadChildren: () => + import('@bulldozer-client/view-instruction-documents').then( + (m) => m.ViewInstructionDocumentsModule + ), + }, + { + path: 'signers', + loadChildren: () => + import('@bulldozer-client/view-instruction-signers').then( + (m) => m.ViewInstructionSignersModule + ), + }, + { + path: 'code-editor', + loadChildren: () => + import('@bulldozer-client/view-instruction-code-editor').then( + (m) => m.ViewInstructionCodeEditorModule + ), + }, + { + path: '', + redirectTo: 'arguments', + }, + ], + }, ]), MatButtonModule, MatProgressSpinnerModule, MatSnackBarModule, MatTooltipModule, ReactiveComponentModule, - PageHeaderModule, - CodeEditorModule, - InstructionArgumentsListModule, - InstructionSignersListModule, - InstructionDocumentsListModule, ItemUpdatingModule, + EditInstructionModule, + HdWalletAdapterCdkModule, + HdBroadcasterCdkModule, ], }) export class ViewInstructionModule {} diff --git a/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction.store.ts b/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction.store.ts index 02f10733..f7ba9ed3 100644 --- a/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction.store.ts +++ b/libs/bulldozer/client/instructions/pages/view-instruction/src/lib/view-instruction.store.ts @@ -1,578 +1,191 @@ import { Injectable } from '@angular/core'; +import { InstructionStore } from '@bulldozer-client/instructions-data-access'; import { - CollectionQueryStore, - CollectionsStore, -} from '@bulldozer-client/collections-data-access'; -import { TabStore } from '@bulldozer-client/core-data-access'; + HdBroadcasterSocketStore, + TransactionStatus, +} from '@heavy-duty/broadcaster'; import { - InstructionAccountApiService, - InstructionAccountQueryStore, - InstructionAccountsStore, - InstructionApiService, - InstructionArgumentApiService, - InstructionArgumentQueryStore, - InstructionArgumentsStore, - InstructionRelationApiService, - InstructionRelationQueryStore, - InstructionRelationsStore, - InstructionStore, -} from '@bulldozer-client/instructions-data-access'; -import { NotificationStore } from '@bulldozer-client/notifications-data-access'; -import { InstructionStatus } from '@bulldozer-client/users-data-access'; -import { WorkspaceInstructionsStore } from '@bulldozer-client/workspaces-data-access'; -import { - InstructionAccountDto, - InstructionArgumentDto, + Document, + flattenInstructions, + Instruction, + InstructionStatus, } from '@heavy-duty/bulldozer-devkit'; -import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; -import { WalletStore } from '@heavy-duty/wallet-adapter'; -import { ComponentStore, tapResponse } from '@ngrx/component-store'; -import { concatMap, EMPTY, map, of, pipe, tap, withLatestFrom } from 'rxjs'; -import { ViewInstructionDocumentsStore } from './view-instruction-documents.store'; -import { ViewInstructionSignersStore } from './view-instruction-signers.store'; +import { isNotNullOrUndefined, isTruthy } from '@heavy-duty/rxjs'; +import { ComponentStore } from '@ngrx/component-store'; +import { TransactionSignature } from '@solana/web3.js'; +import { List } from 'immutable'; +import { EMPTY, switchMap, tap } from 'rxjs'; +import { v4 as uuid } from 'uuid'; +import { reduceInstructions } from './reduce-instructions'; +import { InstructionItemView } from './types'; + +const documentToView = ( + document: Document +): InstructionItemView => { + return { + id: document.id, + name: document.name, + isCreating: false, + isUpdating: false, + isDeleting: false, + applicationId: document.data.application, + workspaceId: document.data.workspace, + }; +}; interface ViewModel { instructionId: string | null; - applicationId: string | null; - workspaceId: string | null; + transactions: List; } const initialState: ViewModel = { instructionId: null, - applicationId: null, - workspaceId: null, + transactions: List(), }; @Injectable() export class ViewInstructionStore extends ComponentStore { readonly instructionId$ = this.select(({ instructionId }) => instructionId); - readonly applicationId$ = this.select(({ applicationId }) => applicationId); - readonly workspaceId$ = this.select(({ workspaceId }) => workspaceId); + private readonly _topicName$ = this.select( + this.instructionId$.pipe(isNotNullOrUndefined), + (instructionId) => `instructions:${instructionId}` + ); + private readonly _instructionStatuses$ = this.select( + this.select(({ transactions }) => transactions), + (transactions) => + transactions + .reduce( + (currentInstructions, transactionStatus) => + currentInstructions.concat(flattenInstructions(transactionStatus)), + List() + ) + .sort( + (a, b) => + a.transactionStatus.timestamp - b.transactionStatus.timestamp + ) + ); + readonly instruction$ = this.select( + this._instructionStore.instruction$, + this._instructionStatuses$, + (instruction, instructionStatuses) => + instructionStatuses.reduce( + reduceInstructions, + instruction === null ? null : documentToView(instruction) + ), + { debounce: true } + ); constructor( - private readonly _walletStore: WalletStore, - private readonly _tabStore: TabStore, - private readonly _notificationStore: NotificationStore, - private readonly _instructionApiService: InstructionApiService, - private readonly _instructionAccountApiService: InstructionAccountApiService, - private readonly _instructionArgumentApiService: InstructionArgumentApiService, - private readonly _instructionRelationApiService: InstructionRelationApiService, - private readonly _instructionStore: InstructionStore, - private readonly _collectionsStore: CollectionsStore, - private readonly _collectionQueryStore: CollectionQueryStore, - private readonly _instructionArgumentsStore: InstructionArgumentsStore, - private readonly _instructionArgumentQueryStore: InstructionArgumentQueryStore, - private readonly _instructionAccountsStore: InstructionAccountsStore, - private readonly _instructionAccountQueryStore: InstructionAccountQueryStore, - private readonly _instructionRelationsStore: InstructionRelationsStore, - private readonly _instructionRelationQueryStore: InstructionRelationQueryStore, - private readonly _viewInstructionSignersStore: ViewInstructionSignersStore, - private readonly _viewInstructionDocumentsStore: ViewInstructionDocumentsStore, - workspaceInstructionsStore: WorkspaceInstructionsStore + private readonly _hdBroadcasterSocketStore: HdBroadcasterSocketStore, + private readonly _instructionStore: InstructionStore ) { super(initialState); - this._instructionStore.setInstructionId(this.instructionId$); - this._viewInstructionSignersStore.setInstructionId(this.instructionId$); - this._viewInstructionDocumentsStore.setInstructionId(this.instructionId$); - this._collectionQueryStore.setFilters( - this.applicationId$.pipe( - isNotNullOrUndefined, - map((application) => ({ application })) - ) - ); - this._collectionsStore.setCollectionIds( - this._collectionQueryStore.collectionIds$ - ); - this._instructionArgumentQueryStore.setFilters( - this.instructionId$.pipe( - isNotNullOrUndefined, - map((instruction) => ({ instruction })) - ) - ); - this._instructionArgumentsStore.setInstructionArgumentIds( - this._instructionArgumentQueryStore.instructionArgumentIds$ - ); - this._instructionAccountQueryStore.setFilters( - this.instructionId$.pipe( - isNotNullOrUndefined, - map((instruction) => ({ instruction })) - ) - ); - this._instructionAccountsStore.setInstructionAccountIds( - this._instructionAccountQueryStore.instructionAccountIds$ - ); - this._instructionRelationQueryStore.setFilters( - this.instructionId$.pipe( - isNotNullOrUndefined, - map((instruction) => ({ instruction })) + this._instructionStore.setInstructionId( + this.select( + this.instructionId$.pipe(isNotNullOrUndefined), + this._hdBroadcasterSocketStore.connected$.pipe(isTruthy), + (instructionId) => instructionId ) ); - this._instructionRelationsStore.setInstructionRelationIds( - this._instructionRelationQueryStore.instructionRelationIds$ - ); - this._handleInstruction(workspaceInstructionsStore.instruction$); - this._openTab( + this._registerTopic( this.select( - this.instructionId$, - this.applicationId$, - this.workspaceId$, - (instructionId, applicationId, workspaceId) => ({ - instructionId, - applicationId, - workspaceId, - }), - { debounce: true } + this._hdBroadcasterSocketStore.connected$, + this._topicName$, + (connected, topicName) => ({ + connected, + topicName, + }) ) ); } - readonly setWorkspaceId = this.updater( - (state, workspaceId) => ({ ...state, workspaceId }) + private readonly _addTransaction = this.updater( + (state, transaction) => ({ + ...state, + transactions: state.transactions.push(transaction), + }) ); - readonly setApplicationId = this.updater( - (state, applicationId) => ({ ...state, applicationId }) + private readonly _removeTransaction = this.updater( + (state, signature) => ({ + ...state, + transactions: state.transactions.filter( + (transaction) => transaction.signature !== signature + ), + }) ); readonly setInstructionId = this.updater( - (state, instructionId) => ({ ...state, instructionId }) - ); - - private readonly _handleInstruction = this.effect( - tap((instructionStatus) => { - switch (instructionStatus.name) { - case 'createInstruction': - case 'updateInstruction': - case 'updateInstructionBody': - case 'deleteInstruction': { - this._instructionStore.dispatch(instructionStatus); - break; - } - case 'createInstructionArgument': - case 'updateInstructionArgument': - case 'deleteInstructionArgument': { - this._instructionArgumentsStore.dispatch(instructionStatus); - break; - } - case 'createInstructionAccount': - case 'updateInstructionAccount': - case 'deleteInstructionAccount': { - this._instructionAccountsStore.dispatch(instructionStatus); - break; - } - case 'createInstructionRelation': - case 'deleteInstructionRelation': { - this._instructionRelationsStore.dispatch(instructionStatus); - break; - } - case 'createCollection': - case 'updateCollection': - case 'deleteCollection': { - this._collectionsStore.dispatch(instructionStatus); - break; - } - default: - break; - } + (state, instructionId) => ({ + ...state, + instructionId, }) ); - private readonly _openTab = this.effect<{ - instructionId: string | null; - applicationId: string | null; - workspaceId: string | null; - }>( - tap(({ instructionId, applicationId, workspaceId }) => { - if ( - instructionId !== null && - applicationId !== null && - workspaceId !== null - ) { - this._tabStore.openTab({ - id: instructionId, - kind: 'instruction', - url: `/workspaces/${workspaceId}/applications/${applicationId}/instructions/${instructionId}`, - }); + private readonly _handleTransaction = this.effect( + tap((transaction) => { + if (transaction.error !== undefined) { + this._removeTransaction(transaction.signature); + } else { + this._addTransaction(transaction); } }) ); - readonly updateInstructionBody = this.effect<{ - workspaceId: string; - applicationId: string; - instructionId: string; - instructionBody: string; - }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap( - ([ - { workspaceId, applicationId, instructionId, instructionBody }, - authority, - ]) => { - if (authority === null) { - return EMPTY; - } - - return this._instructionApiService - .updateBody({ - authority: authority.toBase58(), - workspaceId, - applicationId, - instructionId, - instructionBody, - }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent('Update body request sent'), - (error) => this._notificationStore.setError(error) - ) - ); - } - ) - ) - ); - - readonly createInstructionArgument = this.effect<{ - workspaceId: string; - applicationId: string; - instructionId: string; - instructionArgumentDto: InstructionArgumentDto; - }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap( - ([ - { workspaceId, applicationId, instructionId, instructionArgumentDto }, - authority, - ]) => { - if (authority === null) { - return EMPTY; - } - - return this._instructionArgumentApiService - .create({ - instructionArgumentDto, - authority: authority.toBase58(), - workspaceId, - applicationId, - instructionId, - }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent( - 'Create argument request sent' - ), - (error) => this._notificationStore.setError(error) - ) - ); - } - ) - ) - ); - - readonly updateInstructionArgument = this.effect<{ - workspaceId: string; - instructionId: string; - instructionArgumentId: string; - instructionArgumentDto: InstructionArgumentDto; - }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap( - ([ - { - workspaceId, - instructionId, - instructionArgumentId, - instructionArgumentDto, - }, - authority, - ]) => { - if (authority === null) { - return EMPTY; - } - - return this._instructionArgumentApiService - .update({ - authority: authority.toBase58(), - workspaceId, - instructionId, - instructionArgumentDto, - instructionArgumentId, - }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent( - 'Update argument request sent' - ), - (error) => this._notificationStore.setError(error) - ) - ); - } - ) - ) - ); - - readonly deleteInstructionArgument = this.effect<{ - workspaceId: string; - instructionId: string; - instructionArgumentId: string; - }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap( - ([ - { workspaceId, instructionId, instructionArgumentId }, - authority, - ]) => { - if (authority === null) { - return EMPTY; - } - - return this._instructionArgumentApiService - .delete({ - authority: authority.toBase58(), - workspaceId, - instructionArgumentId, - instructionId, - }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent( - 'Delete argument request sent' - ), - (error) => this._notificationStore.setError(error) - ) - ); - } - ) - ) - ); - - readonly createInstructionAccount = this.effect<{ - workspaceId: string; - applicationId: string; - instructionId: string; - instructionAccountDto: InstructionAccountDto; - }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap( - ([ - { workspaceId, applicationId, instructionId, instructionAccountDto }, - authority, - ]) => { - if (authority === null) { - return EMPTY; - } - - return this._instructionAccountApiService - .create({ - instructionAccountDto, - authority: authority.toBase58(), - workspaceId, - applicationId, - instructionId, - }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent( - 'Create account request sent' - ), - (error) => this._notificationStore.setError(error) - ) - ); - } - ) - ) - ); - - readonly updateInstructionAccount = this.effect<{ - workspaceId: string; - instructionId: string; - instructionAccountId: string; - instructionAccountDto: InstructionAccountDto; - }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap( - ([ - { - workspaceId, - instructionId, - instructionAccountId, - instructionAccountDto, - }, - authority, - ]) => { - if (authority === null) { - return EMPTY; - } - - return this._instructionAccountApiService - .update({ - authority: authority.toBase58(), - workspaceId, - instructionId, - instructionAccountDto, - instructionAccountId, - }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent( - 'Update account request sent' - ), - (error) => this._notificationStore.setError(error) - ) - ); - } - ) - ) - ); - - readonly deleteInstructionAccount = this.effect<{ - workspaceId: string; - instructionId: string; - instructionAccountId: string; - }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap( - ([{ workspaceId, instructionId, instructionAccountId }, authority]) => { - if (authority === null) { - return EMPTY; - } - - return this._instructionAccountApiService - .delete({ - authority: authority.toBase58(), - workspaceId, - instructionAccountId, - instructionId, - }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent( - 'Delete account request sent' - ), - (error) => this._notificationStore.setError(error) - ) - ); - } - ) - ) - ); - - readonly createInstructionRelation = this.effect<{ - workspaceId: string; - applicationId: string; - instructionId: string; - fromAccountId: string; - toAccountId: string; + private readonly _registerTopic = this.effect<{ + connected: boolean; + topicName: string | null; }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap( - ([ - { - workspaceId, - applicationId, - instructionId, - fromAccountId, - toAccountId, - }, - authority, - ]) => { - if (authority === null) { - return EMPTY; - } + switchMap(({ connected, topicName }) => { + if (!connected || topicName === null) { + return EMPTY; + } - return this._instructionRelationApiService - .create({ - fromAccountId, - toAccountId, - authority: authority.toBase58(), - workspaceId, - applicationId, - instructionId, - }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent( - 'Create relation request sent' - ), - (error) => this._notificationStore.setError(error) - ) + this.patchState({ transactions: List() }); + + const correlationId = uuid(); + let subscriptionId: string; + + return this._hdBroadcasterSocketStore + .multiplex( + () => ({ + event: 'subscribe', + data: { + topicName, + correlationId, + }, + }), + () => ({ + event: 'unsubscribe', + data: { topicName, subscriptionId }, + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (message: any) => { + if ( + typeof message === 'object' && + message !== null && + 'data' in message && + 'id' in message.data && + 'subscriptionId' in message.data && + message.data.id === correlationId + ) { + subscriptionId = message.data.subscriptionId; + } + + return ( + message.data.subscriptionId === subscriptionId && + message.data.topicName === topicName ); - } - ) - ) - ); - - readonly deleteInstructionRelation = this.effect<{ - workspaceId: string; - instructionId: string; - fromAccountId: string; - toAccountId: string; - }>( - pipe( - concatMap((request) => - of(request).pipe(withLatestFrom(this._walletStore.publicKey$)) - ), - concatMap( - ([ - { workspaceId, instructionId, fromAccountId, toAccountId }, - authority, - ]) => { - if (authority === null) { - return EMPTY; } - - return this._instructionRelationApiService - .delete({ - authority: authority.toBase58(), - workspaceId, - instructionId, - fromAccountId, - toAccountId, - }) - .pipe( - tapResponse( - () => - this._notificationStore.setEvent( - 'Delete relation request sent' - ), - (error) => this._notificationStore.setError(error) - ) - ); - } - ) - ) + ) + .pipe( + tap((message) => { + if (message.data.transactionStatus) { + this._handleTransaction(message.data.transactionStatus); + } + }) + ); + }) ); } diff --git a/libs/bulldozer/client/users/components/profile-tab/.eslintrc.json b/libs/bulldozer/client/notifications/components/snack-bar/.eslintrc.json similarity index 100% rename from libs/bulldozer/client/users/components/profile-tab/.eslintrc.json rename to libs/bulldozer/client/notifications/components/snack-bar/.eslintrc.json diff --git a/libs/bulldozer/client/notifications/components/snack-bar/README.md b/libs/bulldozer/client/notifications/components/snack-bar/README.md new file mode 100644 index 00000000..a51f23a2 --- /dev/null +++ b/libs/bulldozer/client/notifications/components/snack-bar/README.md @@ -0,0 +1,7 @@ +# bulldozer-client-notifications-components-snack-bar + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test bulldozer-client-notifications-components-snack-bar` to execute the unit tests. diff --git a/libs/bulldozer/client/notifications/components/snack-bar/jest.config.js b/libs/bulldozer/client/notifications/components/snack-bar/jest.config.js new file mode 100644 index 00000000..f1f329bf --- /dev/null +++ b/libs/bulldozer/client/notifications/components/snack-bar/jest.config.js @@ -0,0 +1,22 @@ +module.exports = { + displayName: 'bulldozer-client-notifications-components-snack-bar', + preset: '../../../../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + }, + coverageDirectory: + '../../../../../../coverage/libs/bulldozer/client/notifications/components/snack-bar', + transform: { + '^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular', + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/libs/bulldozer/client/notifications/components/snack-bar/project.json b/libs/bulldozer/client/notifications/components/snack-bar/project.json new file mode 100644 index 00000000..684ed22a --- /dev/null +++ b/libs/bulldozer/client/notifications/components/snack-bar/project.json @@ -0,0 +1,28 @@ +{ + "projectType": "library", + "root": "libs/bulldozer/client/notifications/components/snack-bar", + "sourceRoot": "libs/bulldozer/client/notifications/components/snack-bar/src", + "prefix": "bd", + "targets": { + "test": { + "executor": "@nrwl/jest:jest", + "outputs": [ + "coverage/libs/bulldozer/client/notifications/components/snack-bar" + ], + "options": { + "jestConfig": "libs/bulldozer/client/notifications/components/snack-bar/jest.config.js", + "passWithNoTests": true + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "libs/bulldozer/client/notifications/components/snack-bar/src/**/*.ts", + "libs/bulldozer/client/notifications/components/snack-bar/src/**/*.html" + ] + } + } + }, + "tags": [] +} diff --git a/libs/bulldozer/client/notifications/components/snack-bar/src/index.ts b/libs/bulldozer/client/notifications/components/snack-bar/src/index.ts new file mode 100644 index 00000000..4e26c977 --- /dev/null +++ b/libs/bulldozer/client/notifications/components/snack-bar/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/snack-bar.component'; +export * from './lib/snack-bar.module'; diff --git a/libs/bulldozer/client/notifications/components/snack-bar/src/lib/snack-bar.component.ts b/libs/bulldozer/client/notifications/components/snack-bar/src/lib/snack-bar.component.ts new file mode 100644 index 00000000..24a7cc5c --- /dev/null +++ b/libs/bulldozer/client/notifications/components/snack-bar/src/lib/snack-bar.component.ts @@ -0,0 +1,77 @@ +import { + Component, + HostBinding, + Inject, + Input, + Pipe, + PipeTransform, +} from '@angular/core'; +import { MAT_SNACK_BAR_DATA } from '@angular/material/snack-bar'; + +export interface SnackBarData { + title: string; + message: string; + type: 'error' | 'success' | 'warning'; +} + +@Pipe({ + name: 'bdSnackBarClass', + pure: true, +}) +export class SnackBarClassPipe implements PipeTransform { + transform(value: string | null): string[] { + switch (value) { + case 'success': + return ['bg-green-600', 'bd-box-shadow-bg-green']; + case 'warning': + return ['bg-yellow-600', 'bd-box-shadow-bg-yellow']; + case 'error': + return ['bg-red-600', 'bd-box-shadow-bg-red']; + default: + return ['bg-black-600']; + } + } +} + +@Component({ + selector: 'bd-snack-bar-pole', + template: ` +
+
+
+
+ `, +}) +export class SnackBarPoleComponent { + @HostBinding('class') class = 'flex flex-col items-center px-4'; + @Input() type: string | null = null; +} + +//// + +@Component({ + selector: 'bd-snack-bar', + template: ` +
+ + +
+ +
+

{{ data.title }}

+

{{ data.message }}

+
+ `, + styles: [], +}) +export class SnackBarComponent { + @HostBinding('class') class = 'block relative w-64'; + + constructor( + @Inject(MAT_SNACK_BAR_DATA) + public data: SnackBarData + ) {} +} diff --git a/libs/bulldozer/client/notifications/components/snack-bar/src/lib/snack-bar.module.ts b/libs/bulldozer/client/notifications/components/snack-bar/src/lib/snack-bar.module.ts new file mode 100644 index 00000000..21369e49 --- /dev/null +++ b/libs/bulldozer/client/notifications/components/snack-bar/src/lib/snack-bar.module.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { + SnackBarClassPipe, + SnackBarComponent, + SnackBarPoleComponent, +} from './snack-bar.component'; + +@NgModule({ + imports: [CommonModule], + declarations: [SnackBarPoleComponent, SnackBarClassPipe, SnackBarComponent], + exports: [SnackBarComponent], +}) +export class SnackBarModule {} diff --git a/libs/bulldozer/client/users/components/user-details/src/test-setup.ts b/libs/bulldozer/client/notifications/components/snack-bar/src/test-setup.ts similarity index 100% rename from libs/bulldozer/client/users/components/user-details/src/test-setup.ts rename to libs/bulldozer/client/notifications/components/snack-bar/src/test-setup.ts diff --git a/libs/bulldozer/client/users/components/profile-tab/tsconfig.json b/libs/bulldozer/client/notifications/components/snack-bar/tsconfig.json similarity index 100% rename from libs/bulldozer/client/users/components/profile-tab/tsconfig.json rename to libs/bulldozer/client/notifications/components/snack-bar/tsconfig.json diff --git a/libs/bulldozer/client/users/components/profile-tab/tsconfig.lib.json b/libs/bulldozer/client/notifications/components/snack-bar/tsconfig.lib.json similarity index 100% rename from libs/bulldozer/client/users/components/profile-tab/tsconfig.lib.json rename to libs/bulldozer/client/notifications/components/snack-bar/tsconfig.lib.json diff --git a/libs/bulldozer/client/users/components/profile-tab/tsconfig.spec.json b/libs/bulldozer/client/notifications/components/snack-bar/tsconfig.spec.json similarity index 100% rename from libs/bulldozer/client/users/components/profile-tab/tsconfig.spec.json rename to libs/bulldozer/client/notifications/components/snack-bar/tsconfig.spec.json diff --git a/libs/bulldozer/client/notifications/data-access/src/lib/notification.store.ts b/libs/bulldozer/client/notifications/data-access/src/lib/notification.store.ts index 4d6e5ca2..0451c5cb 100644 --- a/libs/bulldozer/client/notifications/data-access/src/lib/notification.store.ts +++ b/libs/bulldozer/client/notifications/data-access/src/lib/notification.store.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { ProgramError } from '@heavy-duty/anchor'; +import { SnackBarComponent } from '@bulldozer-client/notification-snack-bar'; +import { AnchorError, ProgramError } from '@heavy-duty/anchor'; import { isNotNullOrUndefined } from '@heavy-duty/rxjs'; import { ComponentStore } from '@ngrx/component-store'; import { WalletError } from '@solana/wallet-adapter-base'; @@ -43,8 +44,13 @@ export class NotificationStore extends ComponentStore { isNotNullOrUndefined, switchMap((error) => this._matSnackBar - .open(this.getErrorMessage(error), 'Close', { - panelClass: `error-snackbar`, + .openFromComponent(SnackBarComponent, { + duration: 10000, + data: { + title: 'Oops!!', + message: this.getErrorMessage(error), + type: 'error', + }, }) .afterDismissed() ), @@ -57,9 +63,13 @@ export class NotificationStore extends ComponentStore { isNotNullOrUndefined, switchMap((event) => this._matSnackBar - .open(event, 'Close', { - panelClass: `success-snackbar`, - duration: 2000, + .openFromComponent(SnackBarComponent, { + duration: 5000, + data: { + title: 'Hooray...', + message: event, + type: 'success', + }, }) .afterDismissed() ), @@ -74,6 +84,8 @@ export class NotificationStore extends ComponentStore { return error.name; } else if (error instanceof ProgramError) { return error.message; + } else if (error instanceof AnchorError) { + return error.error.errorMessage; } else { try { console.error(error); diff --git a/libs/bulldozer/client/users/components/user-details/.eslintrc.json b/libs/bulldozer/client/shared/components/card/.eslintrc.json similarity index 100% rename from libs/bulldozer/client/users/components/user-details/.eslintrc.json rename to libs/bulldozer/client/shared/components/card/.eslintrc.json diff --git a/libs/bulldozer/client/shared/components/card/README.md b/libs/bulldozer/client/shared/components/card/README.md new file mode 100644 index 00000000..d98044c5 --- /dev/null +++ b/libs/bulldozer/client/shared/components/card/README.md @@ -0,0 +1,7 @@ +# bulldozer-client-shared-components-card + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test bulldozer-client-shared-components-card` to execute the unit tests. diff --git a/libs/bulldozer/client/users/components/user-details/jest.config.js b/libs/bulldozer/client/shared/components/card/jest.config.js similarity index 80% rename from libs/bulldozer/client/users/components/user-details/jest.config.js rename to libs/bulldozer/client/shared/components/card/jest.config.js index e8022b66..1f5d994f 100644 --- a/libs/bulldozer/client/users/components/user-details/jest.config.js +++ b/libs/bulldozer/client/shared/components/card/jest.config.js @@ -1,5 +1,5 @@ module.exports = { - displayName: 'bulldozer-client-users-components-user-details', + displayName: 'bulldozer-client-shared-components-card', preset: '../../../../../../jest.preset.js', setupFilesAfterEnv: ['/src/test-setup.ts'], globals: { @@ -9,7 +9,7 @@ module.exports = { }, }, coverageDirectory: - '../../../../../../coverage/libs/bulldozer/client/users/components/user-details', + '../../../../../../coverage/libs/bulldozer/client/shared/components/card', transform: { '^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular', }, diff --git a/libs/bulldozer/client/shared/components/card/project.json b/libs/bulldozer/client/shared/components/card/project.json new file mode 100644 index 00000000..448c1bbe --- /dev/null +++ b/libs/bulldozer/client/shared/components/card/project.json @@ -0,0 +1,26 @@ +{ + "projectType": "library", + "root": "libs/bulldozer/client/shared/components/card", + "sourceRoot": "libs/bulldozer/client/shared/components/card/src", + "prefix": "bd", + "targets": { + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/libs/bulldozer/client/shared/components/card"], + "options": { + "jestConfig": "libs/bulldozer/client/shared/components/card/jest.config.js", + "passWithNoTests": true + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "libs/bulldozer/client/shared/components/card/src/**/*.ts", + "libs/bulldozer/client/shared/components/card/src/**/*.html" + ] + } + } + }, + "tags": [] +} diff --git a/libs/bulldozer/client/shared/components/card/src/index.ts b/libs/bulldozer/client/shared/components/card/src/index.ts new file mode 100644 index 00000000..6fbf999e --- /dev/null +++ b/libs/bulldozer/client/shared/components/card/src/index.ts @@ -0,0 +1 @@ +export * from './lib/card.module'; diff --git a/libs/bulldozer/client/shared/components/card/src/lib/card.component.ts b/libs/bulldozer/client/shared/components/card/src/lib/card.component.ts new file mode 100644 index 00000000..191041ca --- /dev/null +++ b/libs/bulldozer/client/shared/components/card/src/lib/card.component.ts @@ -0,0 +1,33 @@ +import { Component, HostBinding } from '@angular/core'; + +@Component({ + selector: 'bd-card', + template: ` + +
+
+
+
+
+
+
+
+
+
+
+
+ `, + styles: [], +}) +export class CardComponent { + @HostBinding('class') class = + 'px-6 py-4 bd-bg-image-11 bg-bd-black relative shadow rounded'; +} diff --git a/libs/bulldozer/client/shared/components/card/src/lib/card.module.ts b/libs/bulldozer/client/shared/components/card/src/lib/card.module.ts new file mode 100644 index 00000000..ee4e13a5 --- /dev/null +++ b/libs/bulldozer/client/shared/components/card/src/lib/card.module.ts @@ -0,0 +1,10 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { CardComponent } from './card.component'; + +@NgModule({ + imports: [CommonModule], + declarations: [CardComponent], + exports: [CardComponent], +}) +export class CardModule {} diff --git a/libs/bulldozer/client/workspaces/components/my-workspaces-list/src/test-setup.ts b/libs/bulldozer/client/shared/components/card/src/test-setup.ts similarity index 100% rename from libs/bulldozer/client/workspaces/components/my-workspaces-list/src/test-setup.ts rename to libs/bulldozer/client/shared/components/card/src/test-setup.ts diff --git a/libs/bulldozer/client/users/components/user-details/tsconfig.json b/libs/bulldozer/client/shared/components/card/tsconfig.json similarity index 100% rename from libs/bulldozer/client/users/components/user-details/tsconfig.json rename to libs/bulldozer/client/shared/components/card/tsconfig.json diff --git a/libs/bulldozer/client/users/components/user-details/tsconfig.lib.json b/libs/bulldozer/client/shared/components/card/tsconfig.lib.json similarity index 100% rename from libs/bulldozer/client/users/components/user-details/tsconfig.lib.json rename to libs/bulldozer/client/shared/components/card/tsconfig.lib.json diff --git a/libs/bulldozer/client/users/components/user-details/tsconfig.spec.json b/libs/bulldozer/client/shared/components/card/tsconfig.spec.json similarity index 100% rename from libs/bulldozer/client/users/components/user-details/tsconfig.spec.json rename to libs/bulldozer/client/shared/components/card/tsconfig.spec.json diff --git a/libs/bulldozer/client/shared/components/tab-list/src/lib/tab-list.component.ts b/libs/bulldozer/client/shared/components/tab-list/src/lib/tab-list.component.ts index 4f9c2643..a7cde4a9 100644 --- a/libs/bulldozer/client/shared/components/tab-list/src/lib/tab-list.component.ts +++ b/libs/bulldozer/client/shared/components/tab-list/src/lib/tab-list.component.ts @@ -10,43 +10,46 @@ import { Tab } from '@bulldozer-client/core-data-access'; @Component({ selector: 'bd-tab-list', template: ` -