feat(common): implemented increaseOwnerQuotaThunk

This commit is contained in:
Matěj Husák
2026-01-20 19:52:49 +01:00
committed by Matěj "Matt" Husák
parent 66c57cc247
commit 4ebd913d43
17 changed files with 245 additions and 36 deletions

View File

@@ -13,6 +13,7 @@
"@evolu/common": "^7.3.0",
"@noble/hashes": "^1.6.1",
"@suite-common/suite-sync-storage": "workspace:*",
"@suite-common/suite-sync-types": "workspace:*",
"@suite-common/suite-types": "workspace:*",
"@suite-common/wallet-config": "workspace:*",
"@suite-common/wallet-types": "workspace:*",

View File

@@ -0,0 +1,29 @@
import { Evolu } from '@evolu/common';
import { SuiteSyncErrorHandler } from '@suite-common/suite-sync-types';
import { asSuiteSyncOwnerId } from '@suite-common/suite-types';
export const createEvoluErrorHandler =
(evolu: Evolu<any>, errorHandler: SuiteSyncErrorHandler) => () => {
const error = evolu.getError();
// early return if there is no error to handle
if (error === null) {
return;
}
switch (error.type) {
case 'ProtocolQuotaError':
errorHandler({
type: 'RelayQuotaExceeded',
ownerId: asSuiteSyncOwnerId(error.ownerId),
});
return;
default:
errorHandler({ type: 'RelayOther', message: JSON.stringify(error) });
return;
}
};

View File

@@ -2,9 +2,11 @@ import { Evolu, EvoluDeps, SimpleName, createEvolu } from '@evolu/common';
import { sha256 } from '@noble/hashes/sha2';
import { bytesToHex } from '@noble/hashes/utils';
import { SuiteSyncErrorHandler } from '@suite-common/suite-sync-types';
import { SuiteSyncOwner } from '@suite-common/suite-types';
import { createEvoluAppOwnerFromTrezorData } from './createEvoluAppOwnerFromTrezorData';
import { createEvoluErrorHandler } from './createEvoluErrorHandler';
import { Schema } from './schema';
// This is a way how to force change of the SQL files. It was useful for development
@@ -12,7 +14,10 @@ import { Schema } from './schema';
// See: https://www.evolu.dev/docs/faq#how-to-delete-opfs-sqlite-in-browser
const VERSION = 7;
type CreateEvoluInstanceFactoryDeps = EvoluDeps;
type CreateEvoluInstanceFactoryDeps = {
suiteSyncErrorHandler: SuiteSyncErrorHandler;
evoluDeps: EvoluDeps;
};
export type CreateEvoluInstance = (params: {
suiteSyncOwner: SuiteSyncOwner;
@@ -45,7 +50,7 @@ export const createEvoluInstanceFactory =
throw databaseName.error;
}
const evolu = createEvolu(deps)(Schema, {
const evolu = createEvolu(deps.evoluDeps)(Schema, {
name: databaseName.value,
// Intentionally no transport, transport will be passed
// later on, so we can change the RelayUrl at any time.
@@ -56,10 +61,7 @@ export const createEvoluInstanceFactory =
encryptionKey: owner.value.encryptionKey,
});
evolu.subscribeError(() => {
const error = evolu.getError();
console.error(JSON.stringify(error));
});
evolu.subscribeError(createEvoluErrorHandler(evolu, deps.suiteSyncErrorHandler));
return evolu;
};

View File

@@ -6,6 +6,7 @@
},
"references": [
{ "path": "../suite-sync-storage" },
{ "path": "../suite-sync-types" },
{ "path": "../suite-types" },
{ "path": "../wallet-config" },
{ "path": "../wallet-types" },

View File

@@ -14,11 +14,13 @@
"dependencies": {
"@reduxjs/toolkit": "2.10.1",
"@suite-common/delegated-identity-key": "workspace:*",
"@suite-common/redux-utils": "workspace:*",
"@suite-common/suite-types": "workspace:*",
"@suite-common/suite-utils": "workspace:*",
"@suite-common/test-utils": "workspace:*",
"@suite-common/wallet-core": "workspace:*",
"@suite-common/wallet-types": "workspace:*",
"@suite-common/wallet-utils": "workspace:*",
"@trezor/connect": "workspace:*",
"@trezor/device-utils": "workspace:*",
"@trezor/env-utils": "workspace:*",

View File

@@ -11,6 +11,16 @@ export const DEFAULT_DEVICE_SIZE_QUOTA = 1024 * 1024;
*/
export const DEFAULT_ACCOUNT_SIZE_QUOTA = Math.round(DEFAULT_DEVICE_SIZE_QUOTA / 100);
/**
* Default increment account size quota that is used for incrementing quota when no account store is left.
*/
export const DEFAULT_ACCOUNT_INCREMENT_SIZE_QUOTA = DEFAULT_ACCOUNT_SIZE_QUOTA;
export const DEFAULT_QUOTA_MANAGER_URL = isDevEnv
? 'https://suite-sync.suite.sldev.cz/quota-manager/'
: 'https://suite-sync.trezor.io/quota-manager/';
/**
* Header used for signing add space to owner requests.
*/
export const EVOLU_SIGN_ADD_SPACE_TO_OWNER_REQUEST_HEADER = 'EvoluAddSpaceToOwnerV1';

View File

@@ -8,15 +8,16 @@ import { DelegatedIdentityKey, SuiteSyncOwnerId } from '@suite-common/suite-type
import { WalletDescriptor } from '@suite-common/wallet-types';
import { prepareChallengeSession } from './challenge/prepareChallengeSession';
import { DEFAULT_ACCOUNT_SIZE_QUOTA } from './constants';
import {
DEFAULT_ACCOUNT_SIZE_QUOTA,
EVOLU_SIGN_ADD_SPACE_TO_OWNER_REQUEST_HEADER,
} from './constants';
import { quotaManagerFetchError, quotaManagerOwnerFetched } from './quotaManagerActions';
import { selectIsQuotaManagerEnabled, selectQuotaManagerBaseUrl } from './quotaManagerSelectors';
import { checkStorageByOwnerId } from './storage/checkStorage';
import { transferStorageThunk } from './storage/transferStorageThunk';
import { prepareMessageBufferEvoluAddSpaceToOwner } from './util/prepareMessageBufferEvoluAddSpaceToOwner';
const EVOLU_SIGN_ADD_SPACE_TO_OWNER_REQUEST_HEADER = 'EvoluAddSpaceToOwnerV1';
type EnsureOwnerHasAllocatedQuotaParams = {
ownerId: SuiteSyncOwnerId;
walletDescriptor: WalletDescriptor;

View File

@@ -0,0 +1,91 @@
import { Dispatch } from '@reduxjs/toolkit';
import {
getProofOfDelegatedIdentity,
getPublicIdentityKeyFromDelegatedKey,
} from '@suite-common/delegated-identity-key';
import { delegatedIdentityKeyCompositionRoot } from '@suite-common/delegated-identity-key/src/delegatedIdentityKeyCompositionRoot';
import { ExtraDependencies } from '@suite-common/redux-utils';
import { SuiteSyncOwnerId, asDelegatedIdentityKey } from '@suite-common/suite-types';
import { selectSelectedDevice } from '@suite-common/wallet-core';
import { isTrezorDeviceWithState, parseDeviceStaticSessionId } from '@suite-common/wallet-utils';
import TrezorConnect from '@trezor/connect';
import { prepareChallengeSession } from './challenge/prepareChallengeSession';
import {
DEFAULT_ACCOUNT_INCREMENT_SIZE_QUOTA,
EVOLU_SIGN_ADD_SPACE_TO_OWNER_REQUEST_HEADER,
} from './constants';
import { selectIsQuotaManagerEnabled, selectQuotaManagerBaseUrl } from './quotaManagerSelectors';
import { transferStorageThunk } from './storage/transferStorageThunk';
import { prepareMessageBufferEvoluAddSpaceToOwner } from './util/prepareMessageBufferEvoluAddSpaceToOwner';
type AllocateMoreOwnerQuotaParams = {
ownerId: SuiteSyncOwnerId;
};
export const increaseOwnerQuotaThunk =
({ ownerId }: AllocateMoreOwnerQuotaParams) =>
async (dispatch: Dispatch, getState: () => any, extra: ExtraDependencies) => {
const isQuotaManagerEnabled = selectIsQuotaManagerEnabled(getState());
if (!isQuotaManagerEnabled) return;
const device = selectSelectedDevice(getState());
if (!device || !isTrezorDeviceWithState(device)) {
return;
}
const { walletDescriptor } = parseDeviceStaticSessionId(device.state.staticSessionId);
const quotaManagerBaseUrl = selectQuotaManagerBaseUrl(getState());
const { ensureDelegatedIdentityKey } = delegatedIdentityKeyCompositionRoot({
dispatch,
getState,
trezorConnect: TrezorConnect,
platformEncryption: extra.services.platformEncryption,
});
const delegatedKey = await ensureDelegatedIdentityKey({ device });
if (!delegatedKey.success) {
return;
}
const delegatedPublicKey = getPublicIdentityKeyFromDelegatedKey(delegatedKey.payload);
const sessionChallenge = await prepareChallengeSession({
baseUrl: quotaManagerBaseUrl,
});
if (!sessionChallenge.success) {
return;
}
const proof = getProofOfDelegatedIdentity({
delegatedKey: asDelegatedIdentityKey(delegatedKey.payload),
header: EVOLU_SIGN_ADD_SPACE_TO_OWNER_REQUEST_HEADER,
appendMessageBuffer: prepareMessageBufferEvoluAddSpaceToOwner({
ownerId,
challenge: sessionChallenge.payload.challenge,
size: DEFAULT_ACCOUNT_INCREMENT_SIZE_QUOTA,
publicKey: delegatedPublicKey,
}),
});
if (!proof.success) {
return;
}
dispatch(
transferStorageThunk({
walletDescriptor,
params: {
ownerId,
size: DEFAULT_ACCOUNT_INCREMENT_SIZE_QUOTA,
proof: proof.payload,
sessionId: sessionChallenge.payload.sessionId,
challenge: sessionChallenge.payload.challenge,
publicKey: delegatedPublicKey,
},
}),
);
};

View File

@@ -7,6 +7,7 @@ export { transferStorageThunk } from './storage/transferStorageThunk';
export { prepareChallengeSession } from './challenge/prepareChallengeSession';
export { ensureDeviceHasQuotaThunk } from './ensureDeviceHasQuotaThunk';
export { ensureOwnerHasAllocatedQuotaThunk } from './ensureOwnerHasAllocatedQuotaThunk';
export { increaseOwnerQuotaThunk } from './increaseOwnerQuotaThunk';
/**
* Actions.

View File

@@ -6,11 +6,13 @@
},
"references": [
{ "path": "../delegated-identity-key" },
{ "path": "../redux-utils" },
{ "path": "../suite-types" },
{ "path": "../suite-utils" },
{ "path": "../test-utils" },
{ "path": "../wallet-core" },
{ "path": "../wallet-types" },
{ "path": "../wallet-utils" },
{ "path": "../../packages/connect" },
{ "path": "../../packages/device-utils" },
{ "path": "../../packages/env-utils" },

View File

@@ -0,0 +1,14 @@
import { Dispatch } from '@reduxjs/toolkit';
import { SuiteSyncOwnerId } from '@suite-common/suite-types';
export type CreateSuiteSyncErrorHandlerDep = {
dispatch: Dispatch;
};
export type RelayQuotaExceededError = { type: 'RelayQuotaExceeded'; ownerId: SuiteSyncOwnerId };
export type SuiteSyncOtherError = { type: 'RelayOther'; message: string };
export type Errors = RelayQuotaExceededError | SuiteSyncOtherError;
export type SuiteSyncErrorHandler = (error: Errors) => void;

View File

@@ -63,3 +63,11 @@ export type {
} from './data/updateWalletLabel';
export type { SuiteSyncAppReloader, SuiteSyncAppReloaderDep } from './suiteSyncAppReloader';
export type {
SuiteSyncErrorHandler,
SuiteSyncOtherError,
RelayQuotaExceededError,
Errors,
CreateSuiteSyncErrorHandlerDep,
} from './SuiteSyncErrorHandler';

View File

@@ -4,8 +4,12 @@ import { EnsureDelegatedIdentityKeyDep } from '@suite-common/delegated-identity-
import { toGetter } from '@suite-common/dependency-injection';
import { PlatformEncryptionDep } from '@suite-common/platform-encryption';
import { selectHasDeviceAllowance } from '@suite-common/suite-sync-quota-manager';
import { CreateSuiteStorageDep, CreateSuiteSyncOwnerDep } from '@suite-common/suite-sync-storage';
import { SuiteSync, SuiteSyncAppReloaderDep } from '@suite-common/suite-sync-types';
import { CreateSuiteStorage, CreateSuiteSyncOwnerDep } from '@suite-common/suite-sync-storage';
import {
SuiteSync,
SuiteSyncAppReloaderDep,
SuiteSyncErrorHandler,
} from '@suite-common/suite-sync-types';
import {
selectAllDeviceStaticIds,
selectDeviceByStaticSessionId,
@@ -14,6 +18,7 @@ import {
import { StaticSessionId } from '@trezor/connect';
import { createRefreshSuiteSync } from './createRefreshSuiteSyncKeys';
import { createSuiteSyncErrorHandler } from './createSuiteSyncErrorHandler';
import { createTurnOffSuiteSync } from './createTurnOffSuiteSync';
import { createTurnOnSuiteSync } from './createTurnOnSuiteSync';
import { createEnsureSubscribeSuiteSyncData } from './data/createEnsureSuiteSyncData';
@@ -39,12 +44,20 @@ import { createSuiteSyncStorageRepository } from './storage/createSuiteSyncStora
import { createTurnOffSuiteSyncForWallet } from './storage/createTurnOffSuiteSyncForWallet';
import { selectIsSuiteSyncEnabled, selectSuiteSyncRelayUrl } from './suiteSyncSelectors';
type CreateSuiteStorageFactory = (deps: {
suiteSyncErrorHandler: SuiteSyncErrorHandler;
}) => CreateSuiteStorage;
type CreateSuiteStorageFactoryDep = {
createSuiteStorageFactory: CreateSuiteStorageFactory;
};
type CreateSuiteSyncCompositionRootDeps = {
getState: () => any;
dispatch: Dispatch;
trezorConnect: RetrieveSuiteSyncOwnerDeps['trezorConnect'];
} & EnsureDelegatedIdentityKeyDep &
CreateSuiteStorageDep &
CreateSuiteStorageFactoryDep &
CreateSuiteSyncOwnerDep &
PlatformEncryptionDep &
SuiteSyncAppReloaderDep;
@@ -90,10 +103,16 @@ export const createSuiteSyncCompositionRoot = (
selectHasDeviceAllowance(deps.getState(), deviceId ?? null, walletDescriptor),
});
const suiteSyncErrorHandler: SuiteSyncErrorHandler = createSuiteSyncErrorHandler({
dispatch: deps.dispatch,
});
const createSuiteStorage = deps.createSuiteStorageFactory({ suiteSyncErrorHandler });
const ensureStorage = createEnsureStorage({
refreshSuiteSyncKeys,
suiteSyncStorageRepository,
createSuiteStorage: deps.createSuiteStorage,
createSuiteStorage,
defaultRelayUrl: DEFAULT_SUITE_SYNC_RELAY_URL,
getRelayUrl: toGetter(deps.getState, selectSuiteSyncRelayUrl),
getDeviceForStaticSessionId,

View File

@@ -0,0 +1,26 @@
import { increaseOwnerQuotaThunk } from '@suite-common/suite-sync-quota-manager';
import {
CreateSuiteSyncErrorHandlerDep,
Errors,
SuiteSyncErrorHandler,
} from '@suite-common/suite-sync-types';
export const createSuiteSyncErrorHandler =
(deps: CreateSuiteSyncErrorHandlerDep): SuiteSyncErrorHandler =>
(error: Errors) => {
switch (error.type) {
case 'RelayQuotaExceeded':
deps.dispatch(
increaseOwnerQuotaThunk({
ownerId: error.ownerId,
}),
);
return;
default:
console.error('SuiteSync relay error', error.message);
return;
}
};

View File

@@ -22,14 +22,17 @@ type SuiteSyncNativeCompositionRootDeps = {
export const createSuiteSyncNativeCompositionRoot = (
deps: SuiteSyncNativeCompositionRootDeps,
): SuiteSync => {
const createEvoluInstance = createEvoluInstanceFactory(evoluReactNativeDeps);
const createEvoluStorage = createEvoluStorageFactory({ createEvoluInstance });
return createSuiteSyncCompositionRoot({
): SuiteSync =>
createSuiteSyncCompositionRoot({
...deps,
createSuiteStorage: createEvoluStorage,
createSuiteStorageFactory: ({ suiteSyncErrorHandler }) => {
const createEvoluInstance = createEvoluInstanceFactory({
evoluDeps: evoluReactNativeDeps,
suiteSyncErrorHandler,
});
return createEvoluStorageFactory({ createEvoluInstance });
},
createSuiteSyncOwner: evoluCreateSuiteSyncOwner,
reloadApp: reloadAppAsync,
});
};

View File

@@ -31,13 +31,18 @@ type SuiteSyncDesktopCompositionRootDeps = {
export const createSuiteSyncDesktopCompositionRoot = (
deps: SuiteSyncDesktopCompositionRootDeps,
): SuiteSync => {
// This is the place where we set Evolu as a SuiteSync Storage.
const createEvoluInstance = createEvoluInstanceFactory(evoluWebDeps);
const createEvoluStorage = createEvoluStorageFactory({ createEvoluInstance });
// This sets up Evolu as a SuiteSync Storage. We provide a factory that
// accepts `suiteSyncErrorHandler` and creates the evolu instance accordingly.
const suiteSync = createSuiteSyncCompositionRoot({
...deps,
createSuiteStorage: createEvoluStorage,
createSuiteStorageFactory: ({ suiteSyncErrorHandler }) => {
const createEvoluInstance = createEvoluInstanceFactory({
evoluDeps: evoluWebDeps,
suiteSyncErrorHandler,
});
return createEvoluStorageFactory({ createEvoluInstance });
},
createSuiteSyncOwner: evoluCreateSuiteSyncOwner,
});

View File

@@ -11050,6 +11050,7 @@ __metadata:
"@evolu/common": "npm:^7.3.0"
"@noble/hashes": "npm:^1.6.1"
"@suite-common/suite-sync-storage": "workspace:*"
"@suite-common/suite-sync-types": "workspace:*"
"@suite-common/suite-types": "workspace:*"
"@suite-common/wallet-config": "workspace:*"
"@suite-common/wallet-types": "workspace:*"
@@ -11063,11 +11064,13 @@ __metadata:
dependencies:
"@reduxjs/toolkit": "npm:2.10.1"
"@suite-common/delegated-identity-key": "workspace:*"
"@suite-common/redux-utils": "workspace:*"
"@suite-common/suite-types": "workspace:*"
"@suite-common/suite-utils": "workspace:*"
"@suite-common/test-utils": "workspace:*"
"@suite-common/wallet-core": "workspace:*"
"@suite-common/wallet-types": "workspace:*"
"@suite-common/wallet-utils": "workspace:*"
"@trezor/connect": "workspace:*"
"@trezor/device-utils": "workspace:*"
"@trezor/env-utils": "workspace:*"
@@ -16844,16 +16847,7 @@ __metadata:
languageName: node
linkType: hard
"@types/react@npm:*, @types/react@npm:>=16":
version: 19.2.8
resolution: "@types/react@npm:19.2.8"
dependencies:
csstype: "npm:^3.2.2"
checksum: 10/688e7605876e2729c25fdfd2c131d7080cb8e98db528aedccab89005bcbca097a6149fa6e137ae4f1807bdc44220e0c5c7b4a968b5b681dadb7761968bac6de5
languageName: node
linkType: hard
"@types/react@npm:19.1.6":
"@types/react@npm:*, @types/react@npm:19.1.6, @types/react@npm:>=16":
version: 19.1.6
resolution: "@types/react@npm:19.1.6"
dependencies:
@@ -22251,7 +22245,7 @@ __metadata:
languageName: node
linkType: hard
"csstype@npm:^3.0.2, csstype@npm:^3.0.5, csstype@npm:^3.1.2, csstype@npm:^3.1.3, csstype@npm:^3.2.2":
"csstype@npm:^3.0.2, csstype@npm:^3.0.5, csstype@npm:^3.1.2, csstype@npm:^3.1.3":
version: 3.2.3
resolution: "csstype@npm:3.2.3"
checksum: 10/ad41baf7e2ffac65ab544d79107bf7cd1a4bb9bab9ac3302f59ab4ba655d5e30942a8ae46e10ba160c6f4ecea464cc95b975ca2fefbdeeacd6ac63f12f99fe1f