diff --git a/.github/workflows/on-tag.yml b/.github/workflows/on-tag.yml index 1447ec4ab..4a6882b81 100644 --- a/.github/workflows/on-tag.yml +++ b/.github/workflows/on-tag.yml @@ -105,8 +105,9 @@ jobs: --cache-to "type=local,dest=/tmp/.buildx-cache,mode=max" \ --platform linux/amd64,linux/arm64 \ --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \ + --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \ --build-context rustgbt=./rust \ --build-context backend=./backend \ --output "type=registry,push=true" \ --build-arg commitHash=$SHORT_SHA \ - ./${{ matrix.service }}/ \ No newline at end of file + ./${{ matrix.service }}/ diff --git a/LICENSE b/LICENSE index b6e67e523..02aa9bac2 100644 --- a/LICENSE +++ b/LICENSE @@ -11,7 +11,7 @@ to use any trademarks, service marks, logos, or trade names of Mempool Space K.K or any other contributor to The Mempool Open Source Project. The Mempool Open Source Project®, Mempool Accelerator®, Mempool Enterprise®, -Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full +Mempool Wallet™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, diff --git a/backend/src/api/services/services-routes.ts b/backend/src/api/services/services-routes.ts index 520496249..281bb1602 100644 --- a/backend/src/api/services/services-routes.ts +++ b/backend/src/api/services/services-routes.ts @@ -17,7 +17,11 @@ class ServicesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 5).toUTCString()); const walletId = req.params.walletId; const wallet = await WalletApi.getWallet(walletId); - res.status(200).send(wallet); + if (wallet === null) { + res.status(404).send('No such wallet'); + } else { + res.status(200).send(wallet); + } } catch (e) { handleError(req, res, 500, 'Failed to get wallet'); } diff --git a/backend/src/api/services/wallets.ts b/backend/src/api/services/wallets.ts index f498a80ad..b68a2e3c4 100644 --- a/backend/src/api/services/wallets.ts +++ b/backend/src/api/services/wallets.ts @@ -4,6 +4,7 @@ import { IEsploraApi } from '../bitcoin/esplora-api.interface'; import bitcoinApi from '../bitcoin/bitcoin-api-factory'; import axios from 'axios'; import { TransactionExtended } from '../../mempool.interfaces'; +import { promises as fsPromises } from 'fs'; interface WalletAddress { address: string; @@ -31,16 +32,98 @@ class WalletApi { private wallets: Record = {}; private syncing = false; private lastSync = 0; + private isSaving = false; + private cacheSchemaVersion = 1; + + private static TMP_FILE_NAME = config.MEMPOOL.CACHE_DIR + '/tmp-wallets-cache.json'; + private static FILE_NAME = config.MEMPOOL.CACHE_DIR + '/wallets-cache.json'; constructor() { this.wallets = config.WALLETS.ENABLED ? (config.WALLETS.WALLETS as string[]).reduce((acc, wallet) => { acc[wallet] = { name: wallet, addresses: {}, lastPoll: 0 }; return acc; }, {} as Record) : {}; + + // Load cache on startup + if (config.WALLETS.ENABLED) { + this.$loadCache(); + } } - public getWallet(wallet: string): Record { - return this.wallets?.[wallet]?.addresses || {}; + private async $loadCache(): Promise { + try { + const cacheData = await fsPromises.readFile(WalletApi.FILE_NAME, 'utf8'); + if (!cacheData) { + return; + } + + const data = JSON.parse(cacheData); + + if (data.cacheSchemaVersion !== this.cacheSchemaVersion) { + logger.notice('Wallets cache contains an outdated schema version. Clearing it.'); + return this.$wipeCache(); + } + + this.wallets = data.wallets; + // Reset lastSync time to force transaction history refresh + for (const wallet of Object.values(this.wallets)) { + wallet.lastPoll = 0; + for (const address of Object.values(wallet.addresses)) { + address.lastSync = 0; + } + } + logger.info('Restored wallets data from disk cache'); + } catch (e) { + logger.warn('Failed to parse wallets cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e)); + } + } + + private async $saveCache(): Promise { + if (this.isSaving || !config.WALLETS.ENABLED) { + return; + } + + try { + this.isSaving = true; + logger.debug('Writing wallets data to disk cache...'); + + const cacheData = { + cacheSchemaVersion: this.cacheSchemaVersion, + wallets: this.wallets, + }; + + await fsPromises.writeFile( + WalletApi.TMP_FILE_NAME, + JSON.stringify(cacheData), + { flag: 'w' } + ); + + await fsPromises.rename(WalletApi.TMP_FILE_NAME, WalletApi.FILE_NAME); + + logger.debug('Wallets data saved to disk cache'); + } catch (e) { + logger.warn('Error writing to wallets cache file: ' + (e instanceof Error ? e.message : e)); + } finally { + this.isSaving = false; + } + } + + private async $wipeCache(): Promise { + try { + await fsPromises.unlink(WalletApi.FILE_NAME); + } catch (e: any) { + if (e?.code !== 'ENOENT') { + logger.err(`Cannot wipe wallets cache file ${WalletApi.FILE_NAME}. Exception ${JSON.stringify(e)}`); + } + } + } + + public getWallet(wallet: string): Record | null { + if (wallet in this.wallets) { + return this.wallets?.[wallet]?.addresses || {}; + } else { + return null; + } } // resync wallet addresses from the services backend @@ -99,6 +182,9 @@ class WalletApi { } wallet.lastPoll = Date.now(); logger.debug(`Synced ${Object.keys(wallet.addresses).length} addresses for wallet ${wallet.name}`); + + // Update cache + await this.$saveCache(); } catch (e) { logger.err(`Error syncing wallet ${wallet.name}: ${(e instanceof Error ? e.message : e)}`); } diff --git a/frontend/custom-river-config.json b/frontend/custom-river-config.json new file mode 100644 index 000000000..aca537adf --- /dev/null +++ b/frontend/custom-river-config.json @@ -0,0 +1,51 @@ +{ + "theme": "wiz", + "enterprise": "river", + "branding": { + "name": "river", + "title": "river", + "site_id": 22, + "header_img": "/resources/riverlogo.svg", + "footer_img": "/resources/riverlogo.svg" + }, + "dashboard": { + "widgets": [ + { + "component": "fees", + "mobileOrder": 4 + }, + { + "component": "walletBalance", + "mobileOrder": 1, + "props": { + "wallet": "RIVER" + } + }, + { + "component": "twitter", + "mobileOrder": 5, + "props": { + "handle": "River" + } + }, + { + "component": "wallet", + "mobileOrder": 2, + "props": { + "wallet": "RIVER", + "period": "all" + } + }, + { + "component": "blocks" + }, + { + "component": "walletTransactions", + "mobileOrder": 3, + "props": { + "wallet": "RIVER" + } + } + ] + } +} \ No newline at end of file diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index 8ac931d7a..c4698b411 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -460,7 +460,7 @@ Trademark Notice

- The Mempool Open Source Project®, Mempool Accelerator®, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries. + The Mempool Open Source Project®, Mempool Accelerator®, Mempool Enterprise®, Mempool Wallet™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.

While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our Trademark Policy and Guidelines for more details, published on <https://mempool.space/trademark-policy>. diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index 4601823dc..652d991f2 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -499,9 +499,13 @@ } @else if (step === 'googlepay') {

} @else if (step === 'cardonfile') { -
- - {{ estimate?.availablePaymentMethods?.cardOnFile?.card?.brand }} {{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }} +
+ @if (['VISA', 'MASTERCARD', 'JCB', 'DISCOVER', 'DISCOVER_DINERS', 'AMERICAN_EXPRESS'].includes(estimate?.availablePaymentMethods?.cardOnFile?.card?.brand)) { + + } @else { + + } + {{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }}
} @if (loadingCashapp || loadingApplePay || loadingGooglePay || loadingCardOnFile) { diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index fda32e065..958a27787 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -61,7 +61,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy { @Input() accelerating: boolean = false; @Input() miningStats: MiningStats; @Input() eta: ETA; - @Input() scrollEvent: boolean; @Input() applePayEnabled: boolean = false; @Input() googlePayEnabled: boolean = true; @Input() cardOnFileEnabled: boolean = true; @@ -191,9 +190,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } ngOnChanges(changes: SimpleChanges): void { - if (changes.scrollEvent && this.scrollEvent) { - this.scrollToElement('acceleratePreviewAnchor', 'start'); - } if (changes.accelerating && this.accelerating) { this.moveToStep('success', true); } @@ -214,6 +210,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { if (this._step === 'checkout') { this.insertSquare(); this.enterpriseService.goal(8); + this.scrollToElementWithTimeout('acceleratePreviewAnchor', 'start', 100); } if (this._step === 'checkout' && this.canPayWithBitcoin) { this.btcpayInvoiceFailed = false; diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index 436cf3c67..291f96f1f 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -86,8 +86,8 @@ -