From 622ffd786b5bda3f7a4a2a70eb41d64d397e4494 Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 25 Sep 2024 16:18:33 +0200 Subject: [PATCH] Adds wallet management to studio app --- apps/api/scripts/script.ts | 6 +- apps/api/scripts/src/safe.ts | 27 ++-- apps/api/src/app/controllers/index.ts | 48 +++---- .../src/app/controllers/pools/pools.router.ts | 24 ++-- .../pools/wallets/delete.controller.ts | 6 +- .../pools/wallets/list.controller.ts | 13 +- .../pools/wallets/post.controller.ts | 17 ++- .../pools/wallets/wallets.router.ts | 10 +- apps/api/src/app/models/Wallet.ts | 1 + apps/api/src/app/services/ERC721Service.ts | 21 +-- apps/api/src/app/services/SafeService.ts | 26 +--- .../src/app/services/TransactionService.ts | 15 ++- apps/api/src/app/services/WalletService.ts | 7 +- apps/api/src/app/util/jest/config.ts | 26 ++-- apps/studio/components.d.ts | 3 + apps/studio/src/assets/logo_hardhat.svg | 19 +++ apps/studio/src/assets/logo_linea.svg | 12 ++ apps/studio/src/assets/logo_metis.svg | 1 + apps/studio/src/assets/logo_polygon.svg | 12 ++ .../components/BaseCardCollectionMetadata.vue | 8 +- .../src/components/BaseModalWalletCreate.vue | 23 ++++ .../BaseModalWalletTransactions.vue | 119 +++++++++++++++++ apps/studio/src/scss/_modals.scss | 47 +------ apps/studio/src/scss/main.scss | 1 + apps/studio/src/stores/Account.ts | 14 ++ apps/studio/src/stores/index.ts | 2 +- apps/studio/src/utils/address.ts | 4 + apps/studio/src/utils/chains.ts | 45 +++++++ apps/studio/src/views/studio/Account.vue | 125 +++++++++++++++++- apps/studio/src/views/studio/Collection.vue | 2 +- apps/studio/src/views/studio/Collections.vue | 13 +- .../src/components/BaseDropdownWallets.vue | 5 +- apps/wallet/src/stores/Account.ts | 6 +- apps/wallet/src/stores/Auth.ts | 13 +- apps/wallet/src/stores/Collectible.ts | 12 +- apps/wallet/src/stores/Entry.ts | 11 +- apps/wallet/src/stores/Wallet.ts | 8 +- apps/wallet/src/stores/Web3Auth.ts | 2 +- apps/wallet/src/views/wallet/Collect.vue | 33 +++-- apps/wallet/src/views/wallet/Overview.vue | 9 +- libs/common/src/lib/types/Account.d.ts | 1 + libs/common/src/lib/types/Wallet.d.ts | 2 + 42 files changed, 581 insertions(+), 218 deletions(-) create mode 100644 apps/studio/src/assets/logo_hardhat.svg create mode 100644 apps/studio/src/assets/logo_linea.svg create mode 100644 apps/studio/src/assets/logo_metis.svg create mode 100644 apps/studio/src/assets/logo_polygon.svg create mode 100644 apps/studio/src/components/BaseModalWalletCreate.vue create mode 100644 apps/studio/src/components/BaseModalWalletTransactions.vue create mode 100644 apps/studio/src/utils/address.ts create mode 100644 apps/studio/src/utils/chains.ts diff --git a/apps/api/scripts/script.ts b/apps/api/scripts/script.ts index 8f18c002a..209e5e30b 100644 --- a/apps/api/scripts/script.ts +++ b/apps/api/scripts/script.ts @@ -8,14 +8,14 @@ import db from '@thxnetwork/api/util/database'; // import main from './src/ipfs'; // import main from './src/invoices'; // import main from './src/demo'; -import main from './src/multisend'; +// import main from './src/multisend'; // import main from './src/preview'; // import main from './src/metamask'; // import main from './src/lottery'; // import main from './src/web3'; -// import main from './src/safe'; +import main from './src/safe'; -db.connect(process.env.MONGODB_URI); +db.connect(process.env.MONGODB_URI_DEV); main() .then(() => process.exit(0)) diff --git a/apps/api/scripts/src/safe.ts b/apps/api/scripts/src/safe.ts index 442ccef5b..02440be49 100644 --- a/apps/api/scripts/src/safe.ts +++ b/apps/api/scripts/src/safe.ts @@ -1,15 +1,20 @@ -import { RewardCoin, Wallet } from '@thxnetwork/api/models'; -import RewardCoinService from '@thxnetwork/api/services/RewardCoinService'; +import { Wallet } from '@thxnetwork/api/models'; +import SafeService from '@thxnetwork/api/services/SafeService'; +import { PromiseParser } from '@thxnetwork/api/util'; +import { WalletVariant } from '@thxnetwork/common/enums'; export default async function main() { - // const reward = await RewardCoin.findById('669126e1110e00291909e0e3'); // Polygon Reward - // const reward = await RewardCoin.findById('66952d939a90f7280b2d3164'); // Linea Reward - const reward = await RewardCoin.findById('6698033a03bf2db6c9a940f1'); // Linea Reward - const wallet = await Wallet.findById('669805738c683b6c4c506e97'); - const service = new RewardCoinService(); + const wallet = await Wallet.find({ variant: WalletVariant.Safe, poolId: { $exists: true } }); + const chunkSize = 10; - await service.createPayment({ - reward, - wallet, - }); + for (let i = 0; i < wallet.length; i += chunkSize) { + await PromiseParser.parse( + wallet.slice(i, i + chunkSize).map(async (w) => { + const safe = await SafeService.getSafe(w); + const owners = await safe.getOwners(); + + await w.updateOne({ owners }); + }), + ); + } } diff --git a/apps/api/src/app/controllers/index.ts b/apps/api/src/app/controllers/index.ts index f66265f6f..3957d6c1f 100644 --- a/apps/api/src/app/controllers/index.ts +++ b/apps/api/src/app/controllers/index.ts @@ -1,35 +1,36 @@ +import { checkJwt, corsHandler } from '@thxnetwork/api/middlewares'; import express from 'express'; -import RouterHealth from './health/health.router'; import RouterAccount from './account/account.router'; -import RouterPools from './pools/pools.router'; -import RouterToken from './token/token.router'; -import RouterParticipants from './participants/participants.router'; -import RouterMetadata from './metadata/metadata.router'; -import RouterUpload from './upload/upload.router'; -import RouterInvites from './invites/invites.router'; +import RouterBrands from './brands/brands.router'; +import RouterCoupons from './coupons/coupons.router'; +import RouterData from './data/data.router'; +import RouterPrices from './earn/earn.router'; +import RouterERC1155 from './erc1155/erc1155.router'; import RouterERC20 from './erc20/erc20.router'; import RouterERC721 from './erc721/erc721.router'; -import RouterERC1155 from './erc1155/erc1155.router'; +import RouterEvents from './events/events.router'; +import RouterHealth from './health/health.router'; +import RouterIdentity from './identity/identity.router'; +import RouterInvites from './invites/invites.router'; +import RouterJobs from './jobs/jobs.router'; +import RouterLeaderboards from './leaderboards/leaderboards.router'; +import RouterLogin from './login/login.router'; +import RouterLotteries from './lotteries/lotteries.router'; +import RouterMetadata from './metadata/metadata.router'; +import RouterOAuth from './oauth/oauth.router'; +import RouterParticipants from './participants/participants.router'; +import RouterPools from './pools/pools.router'; +import RouterWallets from './pools/wallets/wallets.router'; import RouterQRCodes from './qr-codes/qr-codes.router'; -import RouterBrands from './brands/brands.router'; -import RouterWidget from './widget/widget.router'; -import RouterWallet from './wallet/wallet.router'; import RouterQuests from './quests/quests.router'; import RouterRewards from './rewards/rewards.router'; -import RouterLeaderboards from './leaderboards/leaderboards.router'; +import RouterToken from './token/token.router'; +import RouterUpload from './upload/upload.router'; +import RouterVoteEscrow from './ve/ve.router'; +import RouterWallet from './wallet/wallet.router'; import RouterWebhook from './webhook/webhook.router'; -import RouterPrices from './earn/earn.router'; +import RouterWidget from './widget/widget.router'; import RouterWidgets from './widgets/widgets.router'; -import RouterIdentity from './identity/identity.router'; -import RouterEvents from './events/events.router'; -import RouterData from './data/data.router'; -import RouterVoteEscrow from './ve/ve.router'; -import RouterJobs from './jobs/jobs.router'; -import RouterLotteries from './lotteries/lotteries.router'; -import RouterCoupons from './coupons/coupons.router'; -import RouterLogin from './login/login.router'; -import RouterOAuth from './oauth/oauth.router'; -import { checkJwt, corsHandler } from '@thxnetwork/api/middlewares'; const router: express.Router = express.Router({ mergeParams: true }); @@ -62,6 +63,7 @@ router.use('/coupons', RouterCoupons); router.use('/participants', RouterParticipants); router.use('/pools', RouterPools); router.use('/widgets', RouterWidgets); +router.use('/wallets', RouterWallets); router.use('/ve', RouterVoteEscrow); router.use('/erc20', RouterERC20); diff --git a/apps/api/src/app/controllers/pools/pools.router.ts b/apps/api/src/app/controllers/pools/pools.router.ts index 203fd314a..f38dcc98a 100644 --- a/apps/api/src/app/controllers/pools/pools.router.ts +++ b/apps/api/src/app/controllers/pools/pools.router.ts @@ -1,24 +1,23 @@ +import { assertPayment, assertPoolAccess, assertRequestInput } from '@thxnetwork/api/middlewares'; import express from 'express'; -import { assertRequestInput, assertPoolAccess, assertPayment } from '@thxnetwork/api/middlewares'; -import * as ListController from './list.controller'; -import * as ReadController from './get.controller'; -import * as CreateController from './post.controller'; -import * as UpdateController from './patch.controller'; import * as DeleteController from './delete.controller'; import * as CreateDuplicate from './duplicate/post.controller'; +import * as ReadController from './get.controller'; +import * as ListController from './list.controller'; +import * as UpdateController from './patch.controller'; +import * as CreateController from './post.controller'; +import RouterAnalytics from './analytics/analytics.router'; import RouterCollaborators from './collaborators/collaborators.router'; +import RouterER1155 from './erc1155/erc1155.router'; +import RouterERC20 from './erc20/erc20.router'; +import RouterGuilds from './guilds/guilds.router'; +import RouterIntegrations from './integrations/integrations.router'; import RouterParticipants from './participants/participants.router'; -import RouterAnalytics from './analytics/analytics.router'; +import RouterPayments from './payments/payments.router'; import RouterQuests from './quests/quests.router'; import RouterRewards from './rewards/rewards.router'; -import RouterGuilds from './guilds/guilds.router'; -import RouterPayments from './payments/payments.router'; -import RouterERC20 from './erc20/erc20.router'; -import RouterER1155 from './erc1155/erc1155.router'; -import RouterIntegrations from './integrations/integrations.router'; -import RouterWallets from './wallets/wallets.router'; const router: express.Router = express.Router({ mergeParams: true }); @@ -48,6 +47,5 @@ router.use('/:id/participants', RouterParticipants); router.use('/:id/guilds', RouterGuilds); router.use('/:id/erc1155', RouterER1155); router.use('/:id/integrations', RouterIntegrations); -router.use('/:id/wallets', RouterWallets); export default router; diff --git a/apps/api/src/app/controllers/pools/wallets/delete.controller.ts b/apps/api/src/app/controllers/pools/wallets/delete.controller.ts index ccb097dd4..7d0c10706 100644 --- a/apps/api/src/app/controllers/pools/wallets/delete.controller.ts +++ b/apps/api/src/app/controllers/pools/wallets/delete.controller.ts @@ -1,9 +1,9 @@ -import { Request, Response } from 'express'; -import { param } from 'express-validator'; import { Wallet } from '@thxnetwork/api/models'; import { ForbiddenError, NotFoundError } from '@thxnetwork/api/util/errors'; +import { Request, Response } from 'express'; +import { param } from 'express-validator'; -const validation = [param('id').isMongoId(), param('walletId').isMongoId()]; +const validation = [param('walletId').isMongoId()]; const controller = async (req: Request, res: Response) => { const wallet = await Wallet.findById(req.params.walletId); diff --git a/apps/api/src/app/controllers/pools/wallets/list.controller.ts b/apps/api/src/app/controllers/pools/wallets/list.controller.ts index 92dd3a068..529ef4712 100644 --- a/apps/api/src/app/controllers/pools/wallets/list.controller.ts +++ b/apps/api/src/app/controllers/pools/wallets/list.controller.ts @@ -1,16 +1,11 @@ -import { Request, Response } from 'express'; -import { param } from 'express-validator'; -import { Pool, Transaction, Wallet } from '@thxnetwork/api/models'; -import { NotFoundError } from '@thxnetwork/api/util/errors'; +import { Transaction, Wallet } from '@thxnetwork/api/models'; import { PromiseParser } from '@thxnetwork/api/util'; +import { Request, Response } from 'express'; -const validation = [param('id').isMongoId()]; +const validation = []; const controller = async (req: Request, res: Response) => { - const pool = await Pool.findById(req.params.id); - if (!pool) throw new NotFoundError('Pool not found'); - - const wallets = await Wallet.find({ poolId: req.params.id, sub: pool.sub }); + const wallets = await Wallet.find({ sub: req.auth.sub, owners: { $exists: true, $size: 1 } }); const response = await PromiseParser.parse( wallets.map(async (wallet) => { const transactions = await Transaction.find({ walletId: wallet.id }).sort({ createdAt: -1 }).limit(50); diff --git a/apps/api/src/app/controllers/pools/wallets/post.controller.ts b/apps/api/src/app/controllers/pools/wallets/post.controller.ts index f169557b3..911bad48b 100644 --- a/apps/api/src/app/controllers/pools/wallets/post.controller.ts +++ b/apps/api/src/app/controllers/pools/wallets/post.controller.ts @@ -1,21 +1,26 @@ -import { Request, Response } from 'express'; -import { body, param } from 'express-validator'; import { Wallet } from '@thxnetwork/api/models'; -import { ForbiddenError } from '@thxnetwork/api/util/errors'; import { safeVersion } from '@thxnetwork/api/services/ContractService'; +import NetworkService from '@thxnetwork/api/services/NetworkService'; import SafeService from '@thxnetwork/api/services/SafeService'; +import { ForbiddenError } from '@thxnetwork/api/util/errors'; +import { WalletVariant } from '@thxnetwork/common/enums'; +import { Request, Response } from 'express'; +import { body } from 'express-validator'; +import { toChecksumAddress } from 'web3-utils'; -const validation = [param('id').isMongoId(), body('chainId').isInt()]; +const validation = [body('chainId').isInt()]; const controller = async (req: Request, res: Response) => { - const wallet = await Wallet.findOne({ poolId: req.params.id, chainId: req.body.chainId, sub: req.auth.sub }); + const wallet = await Wallet.findOne({ variant: WalletVariant.Safe, chainId: req.body.chainId, sub: req.auth.sub }); if (wallet) throw new ForbiddenError('Wallet for this chain already exists'); + const { defaultAccount } = NetworkService.getProvider(req.body.chainId); + const owners = [toChecksumAddress(defaultAccount)]; const safe = await SafeService.create({ sub: req.auth.sub, chainId: req.body.chainId, - poolId: req.params.id, safeVersion, + owners, }); res.status(201).json(safe); diff --git a/apps/api/src/app/controllers/pools/wallets/wallets.router.ts b/apps/api/src/app/controllers/pools/wallets/wallets.router.ts index 402bb0ae6..01eecbdce 100644 --- a/apps/api/src/app/controllers/pools/wallets/wallets.router.ts +++ b/apps/api/src/app/controllers/pools/wallets/wallets.router.ts @@ -1,13 +1,13 @@ +import { assertRequestInput } from '@thxnetwork/api/middlewares'; import express from 'express'; -import { assertRequestInput, assertPoolAccess } from '@thxnetwork/api/middlewares'; +import * as RemoveWallets from './delete.controller'; import * as ListWallets from './list.controller'; import * as CreateWallets from './post.controller'; -import * as RemoveWallets from './delete.controller'; const router: express.Router = express.Router({ mergeParams: true }); -router.get('/', assertPoolAccess, assertRequestInput(ListWallets.validation), ListWallets.controller); -router.post('/', assertPoolAccess, assertRequestInput(CreateWallets.validation), CreateWallets.controller); -router.delete('/:walletId', assertPoolAccess, assertRequestInput(RemoveWallets.validation), RemoveWallets.controller); +router.get('/', assertRequestInput(ListWallets.validation), ListWallets.controller); +router.post('/', assertRequestInput(CreateWallets.validation), CreateWallets.controller); +router.delete('/:walletId', assertRequestInput(RemoveWallets.validation), RemoveWallets.controller); export default router; diff --git a/apps/api/src/app/models/Wallet.ts b/apps/api/src/app/models/Wallet.ts index 6a0eb30e8..9af92fae5 100644 --- a/apps/api/src/app/models/Wallet.ts +++ b/apps/api/src/app/models/Wallet.ts @@ -15,6 +15,7 @@ export const Wallet = mongoose.model( version: String, safeVersion: String, variant: String, + owners: [String], }, { timestamps: true }, ), diff --git a/apps/api/src/app/services/ERC721Service.ts b/apps/api/src/app/services/ERC721Service.ts index 46462148d..d206ab934 100644 --- a/apps/api/src/app/services/ERC721Service.ts +++ b/apps/api/src/app/services/ERC721Service.ts @@ -1,23 +1,23 @@ -import { keccak256, toUtf8Bytes } from 'ethers/lib/utils'; import { ERC721, ERC721Document } from '@thxnetwork/api/models/ERC721'; import { ERC721Metadata, ERC721MetadataDocument } from '@thxnetwork/api/models/ERC721Metadata'; import { ERC721Token, ERC721TokenDocument } from '@thxnetwork/api/models/ERC721Token'; import { Transaction, TransactionDocument } from '@thxnetwork/api/models/Transaction'; -import { ERC721TokenState, TransactionState } from '@thxnetwork/common/enums'; -import { assertEvent, ExpectedEventNotFound, findEvent, parseLogs } from '@thxnetwork/api/util/events'; import NetworkService from '@thxnetwork/api/services/NetworkService'; +import { assertEvent, ExpectedEventNotFound, findEvent, parseLogs } from '@thxnetwork/api/util/events'; import { paginatedResults } from '@thxnetwork/api/util/pagination'; -import { Wallet, WalletDocument } from '../models/Wallet'; -import { RewardNFT } from '../models/RewardNFT'; -import { getArtifact } from '../hardhat'; -import PoolService from './PoolService'; -import TransactionService from './TransactionService'; -import IPFSService from './IPFSService'; -import WalletService from './WalletService'; +import { ERC721TokenState, TransactionState } from '@thxnetwork/common/enums'; +import { keccak256, toUtf8Bytes } from 'ethers/lib/utils'; import { TransactionReceipt } from 'web3-core'; import { toChecksumAddress } from 'web3-utils'; import { ADDRESS_ZERO } from '../config/secrets'; +import { getArtifact } from '../hardhat'; import { QRCodeEntry } from '../models'; +import { RewardNFT } from '../models/RewardNFT'; +import { Wallet, WalletDocument } from '../models/Wallet'; +import IPFSService from './IPFSService'; +import PoolService from './PoolService'; +import TransactionService from './TransactionService'; +import WalletService from './WalletService'; const contractName = 'THXERC721'; @@ -92,6 +92,7 @@ export async function mint( metadata: ERC721MetadataDocument, ): Promise { const tokenUri = await IPFSService.getTokenURI(erc721, metadata.id); + console.log(tokenUri); return await TransactionService.sendSafeAsync( safe, erc721.address, diff --git a/apps/api/src/app/services/SafeService.ts b/apps/api/src/app/services/SafeService.ts index 4689da1c9..dedd299de 100644 --- a/apps/api/src/app/services/SafeService.ts +++ b/apps/api/src/app/services/SafeService.ts @@ -14,10 +14,7 @@ import { logger } from '../util/logger'; import TransactionService from './TransactionService'; class SafeService { - async create( - data: { sub: string; chainId: ChainId; safeVersion: '1.3.0'; address?: string; poolId?: string }, - userWalletAddress?: string, - ) { + async create(data: { sub: string; chainId: ChainId; safeVersion: '1.3.0'; owners: string[]; address?: string }) { const wallet = await Wallet.create({ variant: WalletVariant.Safe, ...data, @@ -25,27 +22,18 @@ class SafeService { // Present address means Metamask account so do not deploy and return early if (!safeVersion && wallet.address) return wallet; - // Add relayer address and consider this a campaign safe - const { defaultAccount } = NetworkService.getProvider(wallet.chainId); - const owners = [toChecksumAddress(defaultAccount)]; - - // Add user address as a signer and consider this a participant safe - if (userWalletAddress) { - owners.push(toChecksumAddress(userWalletAddress)); - } - - // If campaign safe we provide a nonce based on the timestamp in the MongoID the pool (poolId value) - const saltNonce = wallet.poolId && String(convertObjectIdToNumber(wallet.poolId)); - const safeAddress = await this.deploy(wallet, owners, saltNonce); + // If campaign safe we provide a nonce based on the timestamp in the account sub to make it unique + const saltNonce = wallet.owners.length === 1 && String(convertObjectIdToNumber(wallet.sub)); + const safeAddress = await this.deploy(wallet, saltNonce); return await Wallet.findByIdAndUpdate(wallet.id, { address: safeAddress }, { new: true }); } - async deploy(wallet: WalletDocument, owners: string[], saltNonce?: string) { + async deploy(wallet: WalletDocument, saltNonce?: string) { const { ethAdapter } = NetworkService.getProvider(wallet.chainId); const safeAccountConfig: SafeAccountConfig = { - owners, - threshold: owners.length, + owners: wallet.owners, + threshold: wallet.owners.length, }; const safeAddress = await this.predictAddress(wallet, safeAccountConfig, safeVersion, saltNonce); diff --git a/apps/api/src/app/services/TransactionService.ts b/apps/api/src/app/services/TransactionService.ts index c34ab2af8..a7e5a2557 100644 --- a/apps/api/src/app/services/TransactionService.ts +++ b/apps/api/src/app/services/TransactionService.ts @@ -1,18 +1,18 @@ -import NetworkService from '@thxnetwork/api/services/NetworkService'; -import { ChainId, TransactionState, TransactionType } from '@thxnetwork/common/enums'; +import { RelayerTransactionPayload } from '@openzeppelin/defender-relay-client'; import { MINIMUM_GAS_LIMIT, RELAYER_SPEED } from '@thxnetwork/api/config/secrets'; +import { Transaction, TransactionDocument, Wallet, WalletDocument } from '@thxnetwork/api/models'; +import NetworkService from '@thxnetwork/api/services/NetworkService'; import { paginatedResults } from '@thxnetwork/api/util/pagination'; -import { toChecksumAddress } from 'web3-utils'; import { poll } from '@thxnetwork/api/util/polling'; -import { RelayerTransactionPayload } from '@openzeppelin/defender-relay-client'; -import { Transaction, TransactionDocument, Wallet, WalletDocument } from '@thxnetwork/api/models'; +import { ChainId, TransactionState, TransactionType } from '@thxnetwork/common/enums'; import { TransactionReceipt } from 'web3-core'; +import { toChecksumAddress } from 'web3-utils'; +import { PromiseParser } from '../util'; import { logger } from '../util/logger'; +import ERC1155Service from './ERC1155Service'; import ERC20Service from './ERC20Service'; import ERC721Service from './ERC721Service'; -import ERC1155Service from './ERC1155Service'; import SafeService from './SafeService'; -import { PromiseParser } from '../util'; class TransactionService { /** @@ -304,6 +304,7 @@ class TransactionService { async sendSafeAsync(wallet: WalletDocument, to: string | null, fn: any, callback?: TTransactionCallback) { const data = fn.encodeABI(); + console.log(wallet, to, fn, callback); return await this.proposeSafeAsync(wallet, to, data, callback); } } diff --git a/apps/api/src/app/services/WalletService.ts b/apps/api/src/app/services/WalletService.ts index 39e40df4b..6425f55c8 100644 --- a/apps/api/src/app/services/WalletService.ts +++ b/apps/api/src/app/services/WalletService.ts @@ -1,7 +1,9 @@ import { Transaction } from '@thxnetwork/api/models/Transaction'; import { Wallet } from '@thxnetwork/api/models/Wallet'; import { TransactionState, WalletVariant } from '@thxnetwork/common/enums'; +import { toChecksumAddress } from 'web3-utils'; import { safeVersion } from './ContractService'; +import NetworkService from './NetworkService'; import SafeService from './SafeService'; export default class WalletService { @@ -53,7 +55,10 @@ export default class WalletService { if (safeWallet) throw new Error('Already has a Safe.'); // Deploy a Safe with Web3Auth address and relayer as signers - await SafeService.create({ sub, chainId, safeVersion }, address); + const { defaultAccount } = NetworkService.getProvider(chainId); + const owners = [toChecksumAddress(defaultAccount), toChecksumAddress(address)]; + + await SafeService.create({ sub, chainId, safeVersion, owners }); } static async createWalletConnect({ sub, address }: Partial) { diff --git a/apps/api/src/app/util/jest/config.ts b/apps/api/src/app/util/jest/config.ts index b839508af..2967a01bf 100644 --- a/apps/api/src/app/util/jest/config.ts +++ b/apps/api/src/app/util/jest/config.ts @@ -1,16 +1,17 @@ -import db from '@thxnetwork/api/util/database'; -import { safeVersion } from '@thxnetwork/api/services/ContractService'; -import { AccountPlanType, AccountVariant, ChainId, WalletVariant } from '@thxnetwork/common/enums'; -import { userWalletAddress, userWalletAddress2, userWalletAddress3, userWalletAddress4 } from './constants'; -import { Account, Wallet } from '@thxnetwork/api/models'; -import { poll } from '../polling'; -import { agenda } from '../agenda'; +import { User } from '@supabase/supabase-js'; import { MONGODB_URI, SUPABASE_JWT_SECRET } from '@thxnetwork/api/config/secrets'; +import { Account, Wallet } from '@thxnetwork/api/models'; import { supabase } from '@thxnetwork/api/proxies/AccountProxy'; -import { User } from '@supabase/supabase-js'; +import { safeVersion } from '@thxnetwork/api/services/ContractService'; import NetworkService from '@thxnetwork/api/services/NetworkService'; import SafeService from '@thxnetwork/api/services/SafeService'; +import db from '@thxnetwork/api/util/database'; +import { AccountPlanType, AccountVariant, ChainId, WalletVariant } from '@thxnetwork/common/enums'; import jwt from 'jsonwebtoken'; +import { toChecksumAddress } from 'web3-utils'; +import { agenda } from '../agenda'; +import { poll } from '../polling'; +import { userWalletAddress, userWalletAddress2, userWalletAddress3, userWalletAddress4 } from './constants'; const user = { id: 'uuid_supabase', @@ -96,9 +97,14 @@ class Mock { if (!options.skipWalletCreation) { for (const a of this.accounts) { switch (a.variant) { - case AccountVariant.EmailPassword: - await SafeService.create({ sub: a.sub, chainId, safeVersion }, a.userWalletAddress); + case AccountVariant.EmailPassword: { + // Deploy a Safe with Web3Auth address and relayer as signers + const { defaultAccount } = NetworkService.getProvider(chainId); + const owners = [toChecksumAddress(defaultAccount), toChecksumAddress(a.userWalletAddress)]; + + await SafeService.create({ sub: a.sub, chainId, safeVersion, owners }); break; + } case AccountVariant.Metamask: await Wallet.create({ chainId, diff --git a/apps/studio/components.d.ts b/apps/studio/components.d.ts index ffa0babf4..8a5b3a851 100644 --- a/apps/studio/components.d.ts +++ b/apps/studio/components.d.ts @@ -17,6 +17,8 @@ declare module 'vue' { BaseFormInputFile: typeof import('./src/components/BaseFormInputFile.vue')['default'] BaseHrOrSeparator: typeof import('./src/components/BaseHrOrSeparator.vue')['default'] BaseIcon: typeof import('./src/components/BaseIcon.vue')['default'] + BaseModalWalletCreate: typeof import('./src/components/BaseModalWalletCreate.vue')['default'] + BaseModalWalletTransactions: typeof import('./src/components/BaseModalWalletTransactions.vue')['default'] BAvatar: typeof import('bootstrap-vue-next')['BAvatar'] BBadge: typeof import('bootstrap-vue-next')['BBadge'] BButton: typeof import('bootstrap-vue-next')['BButton'] @@ -55,6 +57,7 @@ declare module 'vue' { BTab: typeof import('bootstrap-vue-next')['BTab'] BTable: typeof import('bootstrap-vue-next')['BTable'] BTabs: typeof import('bootstrap-vue-next')['BTabs'] + copy: typeof import('./src/components/BaseModalWalletTransactions copy.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] } diff --git a/apps/studio/src/assets/logo_hardhat.svg b/apps/studio/src/assets/logo_hardhat.svg new file mode 100644 index 000000000..e5a0da151 --- /dev/null +++ b/apps/studio/src/assets/logo_hardhat.svg @@ -0,0 +1,19 @@ + + + Hardhat + + + + + + + + + + + + + + + + diff --git a/apps/studio/src/assets/logo_linea.svg b/apps/studio/src/assets/logo_linea.svg new file mode 100644 index 000000000..bb56377bd --- /dev/null +++ b/apps/studio/src/assets/logo_linea.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/apps/studio/src/assets/logo_metis.svg b/apps/studio/src/assets/logo_metis.svg new file mode 100644 index 000000000..21ce58721 --- /dev/null +++ b/apps/studio/src/assets/logo_metis.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/studio/src/assets/logo_polygon.svg b/apps/studio/src/assets/logo_polygon.svg new file mode 100644 index 000000000..b29f3d25c --- /dev/null +++ b/apps/studio/src/assets/logo_polygon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/apps/studio/src/components/BaseCardCollectionMetadata.vue b/apps/studio/src/components/BaseCardCollectionMetadata.vue index 221d44ed9..6b884fb6b 100644 --- a/apps/studio/src/components/BaseCardCollectionMetadata.vue +++ b/apps/studio/src/components/BaseCardCollectionMetadata.vue @@ -32,13 +32,7 @@ Remove - - +

Are you sure you want to remove this collectible and all it's QR code entries? This action cannot be undone. Note that minted collectibles can not be removed! diff --git a/apps/studio/src/components/BaseModalWalletCreate.vue b/apps/studio/src/components/BaseModalWalletCreate.vue new file mode 100644 index 000000000..08540f893 --- /dev/null +++ b/apps/studio/src/components/BaseModalWalletCreate.vue @@ -0,0 +1,23 @@ + + + diff --git a/apps/studio/src/components/BaseModalWalletTransactions.vue b/apps/studio/src/components/BaseModalWalletTransactions.vue new file mode 100644 index 000000000..1f65416e2 --- /dev/null +++ b/apps/studio/src/components/BaseModalWalletTransactions.vue @@ -0,0 +1,119 @@ + + + diff --git a/apps/studio/src/scss/_modals.scss b/apps/studio/src/scss/_modals.scss index 98a38a55e..9a98f5e2e 100644 --- a/apps/studio/src/scss/_modals.scss +++ b/apps/studio/src/scss/_modals.scss @@ -1,43 +1,6 @@ -.modal { - --bs-modal-bg: var(--bs-body-bg); - - .modal-content { - border: 0; - } - - .modal-header, - .modal-footer { - border-color: var(--bs-modal-border-color); - } - - .modal-header { - .modal-title { - color: var(--bs-success); - } - - .btn-close { - background: transparent; - color: var(--bs-modal-color); - } - } -} - -.modal-campaign-domain, -.modal-campaign-iframe { - .modal-header, - .modal-body { - background-color: var(--bs-body-bg); - } - .modal-header .btn-close { - color: white; - } -} - -.modal-campaign-iframe .modal-content { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} - -.modal-campaign-iframe .modal-content .modal-body { - line-height: 0; +.modal .modal-header .btn-close { + background: url('data:image/svg+xml,%3csvg xmlns="http://www.w3.org/2000/svg" fill="white" viewBox="0 0 352 512"%3e%3c!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--> ({ account: null as null | TAccount, profiles: [] as TWidget[], + wallets: [] as TWallet[], }), actions: { request(path: string, options?: TRequestOptions) { @@ -28,5 +29,18 @@ export const useAccountStore = defineStore('account', { }); this.profiles = this.profiles.filter((profile) => profile._id !== profileId); }, + async listWallets() { + this.wallets = await this.request('/wallets'); + }, + async removeWallet(walletId: string) { + await this.request(`/wallets/${walletId}`, { + method: 'DELETE', + }); + }, + async removeTransaction(transactionId: string) { + await this.request(`/transactions/${transactionId}`, { + method: 'DELETE', + }); + }, }, }); diff --git a/apps/studio/src/stores/index.ts b/apps/studio/src/stores/index.ts index 1d62e9fd8..0190d64dd 100644 --- a/apps/studio/src/stores/index.ts +++ b/apps/studio/src/stores/index.ts @@ -1,4 +1,4 @@ -export { useAuthStore } from './Auth'; export { useAccountStore } from './Account'; +export { useAuthStore } from './Auth'; export { useCollectionStore } from './Collections'; export { useEntryStore } from './Entries'; diff --git a/apps/studio/src/utils/address.ts b/apps/studio/src/utils/address.ts new file mode 100644 index 000000000..ce9b09fde --- /dev/null +++ b/apps/studio/src/utils/address.ts @@ -0,0 +1,4 @@ +export function shortenAddress(address: `0x${string}` | undefined) { + if (!address) return ''; + return `${address.substring(0, 5)}...${address.substring(address.length - 5, address.length)}`; +} diff --git a/apps/studio/src/utils/chains.ts b/apps/studio/src/utils/chains.ts new file mode 100644 index 000000000..fae8bcb71 --- /dev/null +++ b/apps/studio/src/utils/chains.ts @@ -0,0 +1,45 @@ +import { ChainId } from '@thxnetwork/common/enums'; +import imgLogoHardhat from '@thxnetwork/studio/assets/logo_hardhat.svg'; +import imgLogoLinea from '@thxnetwork/studio/assets/logo_linea.svg'; +import imgLogoMetis from '@thxnetwork/studio/assets/logo_metis.svg'; +import imgLogoPolygon from '@thxnetwork/studio/assets/logo_polygon.svg'; + +export const chainInfo: Record< + number, + { + chainId: ChainId; + name: string; + logo: string; + blockExplorer: string; + safeURL: string; + } +> = { + [ChainId.Polygon]: { + chainId: ChainId.Polygon, + name: 'Polygon', + logo: imgLogoPolygon, + blockExplorer: 'https://polygonscan.com', + safeURL: 'https://app.safe.global', + }, + [ChainId.Linea]: { + chainId: ChainId.Linea, + name: 'Linea', + logo: imgLogoLinea, + blockExplorer: 'https://lineascan.build', + safeURL: 'https://app.safe.global', + }, + [ChainId.Metis]: { + chainId: ChainId.Metis, + name: 'Metis', + logo: imgLogoMetis, + blockExplorer: 'https://explorer.metis.io', + safeURL: 'https://app.safe.global', + }, + [ChainId.Hardhat]: { + chainId: ChainId.Hardhat, + name: 'Hardhat', + logo: imgLogoHardhat, + blockExplorer: 'https://etherscan.io', + safeURL: 'https://app.safe.global', + }, +}; diff --git a/apps/studio/src/views/studio/Account.vue b/apps/studio/src/views/studio/Account.vue index fd9f6832b..139c9cccb 100644 --- a/apps/studio/src/views/studio/Account.vue +++ b/apps/studio/src/views/studio/Account.vue @@ -1,12 +1,16 @@ diff --git a/apps/studio/src/views/studio/Collection.vue b/apps/studio/src/views/studio/Collection.vue index d8f746136..242ac715b 100644 --- a/apps/studio/src/views/studio/Collection.vue +++ b/apps/studio/src/views/studio/Collection.vue @@ -42,9 +42,9 @@ diff --git a/apps/studio/src/views/studio/Collections.vue b/apps/studio/src/views/studio/Collections.vue index a4f6f149d..519ab34c5 100644 --- a/apps/studio/src/views/studio/Collections.vue +++ b/apps/studio/src/views/studio/Collections.vue @@ -31,18 +31,7 @@ Remove - - +

Are you sure you want to remove this collection, all it's collectibles and QR code entries? This action cannot be undone. diff --git a/apps/wallet/src/components/BaseDropdownWallets.vue b/apps/wallet/src/components/BaseDropdownWallets.vue index 7f8565e15..fa9bec5e9 100644 --- a/apps/wallet/src/components/BaseDropdownWallets.vue +++ b/apps/wallet/src/components/BaseDropdownWallets.vue @@ -4,12 +4,13 @@ {{ walletStore.wallet.short }} - + + {{ w.short }} diff --git a/apps/wallet/src/stores/Account.ts b/apps/wallet/src/stores/Account.ts index 52b45666a..fc653f86d 100644 --- a/apps/wallet/src/stores/Account.ts +++ b/apps/wallet/src/stores/Account.ts @@ -1,7 +1,7 @@ import { defineStore } from 'pinia'; -import { useAuthStore } from './Auth'; -import { getStyles } from '../utils/theme'; import { decodeHTML } from '../utils/decode-html'; +import { getStyles } from '../utils/theme'; +import { useAuthStore } from './Auth'; export const useAccountStore = defineStore('account', { state: () => ({ @@ -14,7 +14,7 @@ export const useAccountStore = defineStore('account', { return useAuthStore().request(path, options); }, async get() { - this.account = await this.request('/account'); + this.account = await this.request('/account', { isAuthenticated: true }); }, async getSettings(widgetId: string) { const settings = await this.request(`/widget/${widgetId}`); diff --git a/apps/wallet/src/stores/Auth.ts b/apps/wallet/src/stores/Auth.ts index a0825b426..11f4cc819 100644 --- a/apps/wallet/src/stores/Auth.ts +++ b/apps/wallet/src/stores/Auth.ts @@ -22,11 +22,19 @@ supabase.auth.onAuthStateChange(async (event, session) => { export const useAuthStore = defineStore('auth', { state: () => ({ session: null as null | Session, - isAuthenticated: false, isModalLoginShown: false, }), + getters: { + isAuthenticated(state) { + return !!state.session && state.session.expires_in > 0; + }, + }, actions: { async request(path: string, options?: TRequestOptions) { + // Return if not authenticated + if (!this.isAuthenticated && options?.isAuthenticated) return; + + // Create URL and append params const url = new URL(API_URL); url.pathname = `/v1${path}`; if (options?.params) { @@ -45,6 +53,7 @@ export const useAuthStore = defineStore('auth', { ...(options && options.headers), }, }); + try { return await response.json(); } catch (error) { @@ -65,12 +74,10 @@ export const useAuthStore = defineStore('auth', { await useAccountStore().get(); useWalletStore().list(); - this.isAuthenticated = true; this.isModalLoginShown = false; }, onSignedOut(_session: Session) { this.session = null; - this.isAuthenticated = false; router.push({ name: 'wallet' }); }, async signInWithOtp({ email }: { email: string }) { diff --git a/apps/wallet/src/stores/Collectible.ts b/apps/wallet/src/stores/Collectible.ts index a50f7c27d..2e407bd79 100644 --- a/apps/wallet/src/stores/Collectible.ts +++ b/apps/wallet/src/stores/Collectible.ts @@ -1,6 +1,6 @@ import { defineStore } from 'pinia'; -import { useAuthStore } from './Auth'; import { useWalletStore } from '.'; +import { useAuthStore } from './Auth'; export const useCollectibleStore = defineStore('collectible', { state: () => ({ @@ -10,9 +10,13 @@ export const useCollectibleStore = defineStore('collectible', { request(path: string, options?: TRequestOptions) { return useAuthStore().request(path, options); }, - async list(walletId: string) { - const { chainId } = useWalletStore(); - this.collectibles = await this.request('/erc721/token', { params: { walletId, chainId } }); + async list() { + const { wallet, chainId } = useWalletStore(); + const collectibles = await this.request('/erc721/token', { + isAuthenticated: true, + params: { walletId: wallet?._id, chainId }, + }); + this.collectibles = collectibles.filter((c: TERC721Token) => c.nft && c.metadata); }, }, }); diff --git a/apps/wallet/src/stores/Entry.ts b/apps/wallet/src/stores/Entry.ts index 7e6afcfd3..28930a450 100644 --- a/apps/wallet/src/stores/Entry.ts +++ b/apps/wallet/src/stores/Entry.ts @@ -1,6 +1,6 @@ import { defineStore } from 'pinia'; -import { useAuthStore } from './Auth'; import { useWalletStore } from '.'; +import { useAuthStore } from './Auth'; export const useEntryStore = defineStore('entry', { state: () => ({ @@ -13,7 +13,8 @@ export const useEntryStore = defineStore('entry', { return useAuthStore().request(path, options); }, async get(uuid: string) { - const { entry, metadata, erc721 } = await this.request('/qr-codes/' + uuid); + const { entry, metadata, erc721, error } = await this.request('/qr-codes/' + uuid); + if (error) throw new Error(error.message); this.entry = entry; this.erc721 = erc721; @@ -23,7 +24,11 @@ export const useEntryStore = defineStore('entry', { const { wallet } = useWalletStore(); if (!wallet) throw new Error('Wallet not found'); - await this.request(`/qr-codes/${uuid}`, { method: 'PATCH', params: { walletId: wallet._id } }); + await this.request(`/qr-codes/${uuid}`, { + isAuthenticated: true, + method: 'PATCH', + params: { walletId: wallet._id }, + }); }, }, }); diff --git a/apps/wallet/src/stores/Wallet.ts b/apps/wallet/src/stores/Wallet.ts index 9a1b4e06f..842d710f4 100644 --- a/apps/wallet/src/stores/Wallet.ts +++ b/apps/wallet/src/stores/Wallet.ts @@ -12,12 +12,12 @@ import { import { createWeb3Modal, defaultWagmiConfig } from '@web3modal/wagmi'; import { Web3Modal } from '@web3modal/wagmi/dist/types/src/client'; import { defineStore } from 'pinia'; -import { mainnet } from 'viem/chains'; +import { linea, mainnet, polygon } from 'viem/chains'; import { WALLET_CONNECT_PROJECT_ID, WALLET_URL } from '../config/secrets'; import { useAuthStore } from './Auth'; const wagmiConfig = defaultWagmiConfig({ - chains: [mainnet], + chains: [mainnet, polygon, linea], projectId: WALLET_CONNECT_PROJECT_ID, metadata: { name: 'TwinStory', @@ -45,7 +45,7 @@ export const useWalletStore = defineStore('wallet', { this.wallet = wallet; }, async list() { - const wallets = await this.request('/account/wallets'); + const wallets = (await this.request('/account/wallets', { isAuthenticated: true })) || []; this.wallets = wallets.filter((w: { poolId: string }) => !w.poolId); if (this.wallets.length) { @@ -53,7 +53,7 @@ export const useWalletStore = defineStore('wallet', { } }, async create(body: Partial<{ variant: WalletVariant; message: string; signature: string; chainId: ChainId }>) { - await this.request('/account/wallets', { method: 'POST', body }); + await this.request('/account/wallets', { method: 'POST', body, isAuthenticated: true }); await this.list(); this.set(this.wallets[this.wallets.length - 1]); }, diff --git a/apps/wallet/src/stores/Web3Auth.ts b/apps/wallet/src/stores/Web3Auth.ts index 54142e449..1ca7965ae 100644 --- a/apps/wallet/src/stores/Web3Auth.ts +++ b/apps/wallet/src/stores/Web3Auth.ts @@ -20,7 +20,7 @@ export const useWeb3AuthStore = defineStore('web3auth', { return useAuthStore().request(path, options); }, async getJWT() { - const { jwt } = await this.request('/login/jwt', { method: 'POST' }); + const { jwt } = await this.request('/login/jwt', { method: 'POST', isAuthenticated: true }); return jwt; }, async getPrivateKey() { diff --git a/apps/wallet/src/views/wallet/Collect.vue b/apps/wallet/src/views/wallet/Collect.vue index 82726aedc..aa7d72be3 100644 --- a/apps/wallet/src/views/wallet/Collect.vue +++ b/apps/wallet/src/views/wallet/Collect.vue @@ -1,6 +1,11 @@