From 30c038c2ad985626deebafebba86b877e680b992 Mon Sep 17 00:00:00 2001 From: Reza Rahemtola Date: Sun, 29 Sep 2024 23:18:56 +0900 Subject: [PATCH] feat(subscriptions): Basic frontend working with Hold subscriptions --- .env.example | 3 ++ quasar.config.js | 2 +- src/apis/subscriptions/services.gen.ts | 18 +++---- src/apis/subscriptions/types.gen.ts | 22 ++++----- src/boot/axios.ts | 8 +++ src/pages/Subscriptions.vue | 42 ++++++++++++++++ src/router/routes.ts | 4 ++ src/stores/account.ts | 6 ++- src/stores/settings.ts | 2 +- src/stores/subscription.ts | 68 ++++++++++++++++++++++++++ src/utils/aleph-persistent-storage.ts | 2 +- 11 files changed, 152 insertions(+), 25 deletions(-) create mode 100644 src/boot/axios.ts create mode 100644 src/pages/Subscriptions.vue create mode 100644 src/stores/subscription.ts diff --git a/.env.example b/.env.example index fb1e341..0ffa328 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,5 @@ # Use the testnet in development # ALEPH_API_URL=https://api.twentysix.testnet.network + +# APIs +LTAI_SUBSCRIPTIONS_API_URL=http://localhost:8000 diff --git a/quasar.config.js b/quasar.config.js index f02a537..a31f06d 100644 --- a/quasar.config.js +++ b/quasar.config.js @@ -30,7 +30,7 @@ module.exports = configure(function (ctx) { // app boot file (/src/boot) // --> boot files are part of "main.js" // https://v2.quasar.dev/quasar-cli-vite/boot-files - boot: ['utils', 'wagmi'], + boot: ['axios', 'utils', 'wagmi'], // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css css: ['app.scss', 'tailwind.css'], diff --git a/src/apis/subscriptions/services.gen.ts b/src/apis/subscriptions/services.gen.ts index 47139a0..e8e1ad0 100644 --- a/src/apis/subscriptions/services.gen.ts +++ b/src/apis/subscriptions/services.gen.ts @@ -1,10 +1,18 @@ // This file is auto-generated by @hey-api/openapi-ts import { createClient, createConfig, type Options } from '@hey-api/client-axios'; -import type { SubscribeHoldSubscriptionPostData, SubscribeHoldSubscriptionPostError, SubscribeHoldSubscriptionPostResponse, UnsubscribeHoldSubscriptionDeleteData, UnsubscribeHoldSubscriptionDeleteError, UnsubscribeHoldSubscriptionDeleteResponse, RefreshActiveHoldSubscriptionsHoldRefreshPostError, RefreshActiveHoldSubscriptionsHoldRefreshPostResponse, HoldSubscriptionMessagesHoldMessageGetData, HoldSubscriptionMessagesHoldMessageGetError, HoldSubscriptionMessagesHoldMessageGetResponse, RefreshSubsRefreshPostError, RefreshSubsRefreshPostResponse, GetUserSubscriptionsSubscriptionsGetData, GetUserSubscriptionsSubscriptionsGetError, GetUserSubscriptionsSubscriptionsGetResponse } from './types.gen'; +import type { GetUserSubscriptionsSubscriptionsGetData, GetUserSubscriptionsSubscriptionsGetError, GetUserSubscriptionsSubscriptionsGetResponse, SubscribeHoldSubscriptionPostData, SubscribeHoldSubscriptionPostError, SubscribeHoldSubscriptionPostResponse, UnsubscribeHoldSubscriptionDeleteData, UnsubscribeHoldSubscriptionDeleteError, UnsubscribeHoldSubscriptionDeleteResponse, RefreshActiveHoldSubscriptionsHoldRefreshPostError, RefreshActiveHoldSubscriptionsHoldRefreshPostResponse, HoldSubscriptionMessagesHoldMessageGetData, HoldSubscriptionMessagesHoldMessageGetError, HoldSubscriptionMessagesHoldMessageGetResponse, RefreshSubsRefreshPostError, RefreshSubsRefreshPostResponse } from './types.gen'; export const client = createClient(createConfig()); +/** + * Get User Subscriptions + */ +export const getUserSubscriptionsSubscriptionsGet = (options: Options) => { return (options?.client ?? client).get({ + ...options, + url: '/subscriptions' +}); }; + /** * Subscribe */ @@ -45,12 +53,4 @@ export const holdSubscriptionMessagesHoldMessageGet = (options?: Options) => { return (options?.client ?? client).post({ ...options, url: '/subs/refresh' -}); }; - -/** - * Get User Subscriptions - */ -export const getUserSubscriptionsSubscriptionsGet = (options: Options) => { return (options?.client ?? client).get({ - ...options, - url: '/subscriptions' }); }; \ No newline at end of file diff --git a/src/apis/subscriptions/types.gen.ts b/src/apis/subscriptions/types.gen.ts index fe3e8c9..ab029a9 100644 --- a/src/apis/subscriptions/types.gen.ts +++ b/src/apis/subscriptions/types.gen.ts @@ -79,6 +79,16 @@ export type ValidationError = { type: string; }; +export type GetUserSubscriptionsSubscriptionsGetData = { + query: { + address: string; + }; +}; + +export type GetUserSubscriptionsSubscriptionsGetResponse = (GetUserSubscriptionsResponse); + +export type GetUserSubscriptionsSubscriptionsGetError = (HTTPValidationError); + export type SubscribeHoldSubscriptionPostData = { body: HoldPostSubscriptionBody; }; @@ -111,14 +121,4 @@ export type HoldSubscriptionMessagesHoldMessageGetError = (HTTPValidationError); export type RefreshSubsRefreshPostResponse = (SubsPostRefreshSubscriptionsResponse); -export type RefreshSubsRefreshPostError = unknown; - -export type GetUserSubscriptionsSubscriptionsGetData = { - query: { - address: string; - }; -}; - -export type GetUserSubscriptionsSubscriptionsGetResponse = (GetUserSubscriptionsResponse); - -export type GetUserSubscriptionsSubscriptionsGetError = (HTTPValidationError); \ No newline at end of file +export type RefreshSubsRefreshPostError = unknown; \ No newline at end of file diff --git a/src/boot/axios.ts b/src/boot/axios.ts new file mode 100644 index 0000000..e9f10dc --- /dev/null +++ b/src/boot/axios.ts @@ -0,0 +1,8 @@ +import { boot } from 'quasar/wrappers'; +import { client } from 'src/apis/subscriptions/services.gen'; + +export default boot(() => { + client.setConfig({ + baseURL: process.env.LTAI_SUBSCRIPTIONS_API_URL, + }); +}); diff --git a/src/pages/Subscriptions.vue b/src/pages/Subscriptions.vue new file mode 100644 index 0000000..9c18174 --- /dev/null +++ b/src/pages/Subscriptions.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/router/routes.ts b/src/router/routes.ts index 07d82b3..839123f 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -32,6 +32,10 @@ const routes = [ path: 'persona-management', component: () => import('pages/PersonaManagement.vue'), }, + { + path: 'subscriptions', + component: () => import('pages/Subscriptions.vue'), + }, ], }, diff --git a/src/stores/account.ts b/src/stores/account.ts index 6a3e517..517c3d3 100644 --- a/src/stores/account.ts +++ b/src/stores/account.ts @@ -6,6 +6,7 @@ import { config } from 'src/config/wagmi'; import { base } from '@wagmi/vue/chains'; import { useTokensStore } from 'stores/tokens'; import { useKnowledgeStore } from 'stores/knowledge'; +import { useSubscriptionStore } from 'stores/subscription'; const LTAI_BASE_ADDRESS = '0xF8B1b47AA748F5C7b5D0e80C726a843913EB573a'; @@ -23,12 +24,13 @@ export const useAccountStore = defineStore('account', { async onAccountChange() { const tokensStore = useTokensStore(); const knowledgeStore = useKnowledgeStore(); + const subscriptionsStore = useSubscriptionStore(); this.ltaiBalance = await this.getLTAIBalance(); await this.initAlephStorage(); - await tokensStore.update(); - await knowledgeStore.load(); + + await Promise.all([tokensStore.update(), knowledgeStore.load(), subscriptionsStore.load()]); }, async initAlephStorage() { diff --git a/src/stores/settings.ts b/src/stores/settings.ts index c5f234c..d7d263c 100644 --- a/src/stores/settings.ts +++ b/src/stores/settings.ts @@ -61,7 +61,7 @@ export const useSettingsStore = defineStore('settings', { }, async persistOnAleph(settings: SettingsPersistedOnAleph) { - const account: any = useAccountStore(); + const account = useAccountStore(); if (account.alephStorage !== null) { await account.alephStorage.saveSettings(settings); diff --git a/src/stores/subscription.ts b/src/stores/subscription.ts new file mode 100644 index 0000000..8c9c9d9 --- /dev/null +++ b/src/stores/subscription.ts @@ -0,0 +1,68 @@ +import { defineStore } from 'pinia'; +import { + BaseSubscription, + getUserSubscriptionsSubscriptionsGet, + holdSubscriptionMessagesHoldMessageGet, + subscribeHoldSubscriptionPost, + SubscriptionType, +} from 'src/apis/subscriptions'; +import { getAccount, signMessage } from '@wagmi/core'; +import { config } from 'src/config/wagmi'; + +type SubscriptionState = { + subscriptions: BaseSubscription[]; + isLoaded: boolean; +}; + +export const useSubscriptionStore = defineStore('subscriptions', { + state: (): SubscriptionState => ({ + subscriptions: [], + isLoaded: false, + }), + actions: { + async load() { + const account = getAccount(config); + const address = account.address; + + if (address === undefined) { + return; + } + + const response = await getUserSubscriptionsSubscriptionsGet({ query: { address } }); + + this.subscriptions = response.data?.subscriptions ?? []; + this.isLoaded = true; + }, + + async holdSubscribe(subscriptionType: SubscriptionType) { + const account = getAccount(config); + const address = account.address; + + if (address === undefined) { + return; + } + + const messagesResponse = await holdSubscriptionMessagesHoldMessageGet({ + query: { subscription_type: subscriptionType }, + }); + + if (messagesResponse.data === undefined) { + throw new Error( + messagesResponse.error.detail?.toString() ?? 'Unable to fetch the message to sign to subscribe', + ); + } + + const messageToSign = messagesResponse.data.subscribe_message; + const hash = await signMessage(config, { message: messageToSign }); + + const subscriptionResponse = await subscribeHoldSubscriptionPost({ + body: { + signature: hash, + type: 'standard', + account: { chain: 'base', address }, + }, + }); + // TODO: handle errors and success + }, + }, +}); diff --git a/src/utils/aleph-persistent-storage.ts b/src/utils/aleph-persistent-storage.ts index f2eed2c..f681d8e 100644 --- a/src/utils/aleph-persistent-storage.ts +++ b/src/utils/aleph-persistent-storage.ts @@ -34,7 +34,7 @@ export class AlephPersistentStorage { private encryptionPrivateKey: PrivateKey, ) {} - static async signBaseMessage() { + static signBaseMessage() { return signMessage(config, { message: MESSAGE }); }