Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const SCHEMA_VERSION = 9
export const SCHEMA_VERSION = 10
export const MESSAGING_TIMEOUT = 3 * 60 * 1000
// Shielded (Orchard) Halo2 proofs are CPU-heavy and run well past the normal
// timeout in the popup — give these calls a much longer window.
Expand Down
45 changes: 45 additions & 0 deletions src/content-script/api/PrivateAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ import { CreateStateTransitionHandler } from './private/stateTransitions/createS
import { CreateIdentityPrivateKeyHandler } from './private/identities/createIdentityPrivateKey'
import { AssetLockFundingAddressesRepository } from '../repository/AssetLockFundingAddressesRepository'
import { CoreExplorerService } from '../services/CoreExplorerService'
import { IdentityFundingService } from '../services/IdentityFundingService'
import { canConflictWithFunding, findConflictingFunding } from './fundingConflicts'
import { PrepareIdentityFundingHandler } from './private/identities/prepareIdentityFunding'
import { GetIdentityFundingSourcesHandler } from './private/identities/getIdentityFundingSources'
import { GetIdentityFundingOperationsHandler } from './private/identities/getIdentityFundingOperations'
import { CancelIdentityFundingHandler } from './private/identities/cancelIdentityFunding'
import { RegisterIdentityFromCoreHandler } from './private/identities/registerIdentityFromCore'
import { RegisterIdentityFromPlatformAddressHandler } from './private/identities/registerIdentityFromPlatformAddress'
import { TopUpIdentityFromPlatformAddressHandler } from './private/identities/topUpIdentityFromPlatformAddress'
import { RegisterIdentityFromShieldedPoolHandler } from './private/identities/registerIdentityFromShieldedPool'
import { TopUpIdentityFromCoreHandler } from './private/identities/topUpIdentityFromCore'
import { RequestAssetLockFundingAddressHandler } from './private/assetLocks/requestAssetLockFundingAddress'
import { RequestTopUpFundingAddressHandler } from './private/assetLocks/requestTopUpFundingAddress'
import { RegisterIdentityHandler } from './private/identities/registerIdentity'
Expand Down Expand Up @@ -89,6 +100,8 @@ export class PrivateAPI {
[key: string]: APIHandler
}

identityFunding: IdentityFundingService

async handleMessage (data: EventData): Promise<any> {
const { method, payload } = data

Expand All @@ -104,9 +117,30 @@ export class PrivateAPI {
throw new Error(`Invalid payload: ${validation}`)
}

if (canConflictWithFunding(method)) {
await this.assertNoConflictingFunding(method)
}

return await handler.handle(data)
}

// Refuses a request that would draw on what a pending identity funding
// operation of the selected wallet has reserved.
private async assertNoConflictingFunding (method: string): Promise<void> {
const wallet = await this.identityFunding.walletRepository.getCurrent()

if (wallet == null) {
return
}

const operations = await this.identityFunding.repository({ walletId: wallet.walletId, network: wallet.network }).getAll()
const conflicting = findConflictingFunding(method, operations)

if (conflicting != null) {
throw new Error(`Resume or cancel the pending ${conflicting.source} identity funding operation first`)
}
}

/**
* Builds the handler table. Transport is deliberately NOT registered here —
* the hosting context owns it (see src/offscreen/index.ts), so the same
Expand All @@ -122,6 +156,8 @@ export class PrivateAPI {
const assetLockFundingAddressesRepository = new AssetLockFundingAddressesRepository(this.storageAdapter)
const walletSettingsRepository = new WalletSettingsRepository(this.storageAdapter)
const coreExplorer = new CoreExplorerService()
const identityFunding = new IdentityFundingService(walletRepository, this.sdk, this.coreSDK, coreExplorer)
this.identityFunding = identityFunding

this.handlers = {
[MessagingMethods.GET_STATUS]: new GetStatusHandler(this.storageAdapter, walletRepository),
Expand Down Expand Up @@ -177,6 +213,15 @@ export class PrivateAPI {
[MessagingMethods.GET_CORE_RECEIVE_ADDRESS]: new GetCoreReceiveAddressHandler(walletRepository, coreExplorer, this.sdk),
[MessagingMethods.LIST_CORE_ADDRESSES]: new ListCoreAddressesHandler(walletRepository, coreExplorer, this.sdk),
[MessagingMethods.GET_CORE_BALANCE]: new GetCoreBalanceHandler(walletRepository, coreExplorer),
[MessagingMethods.PREPARE_IDENTITY_FUNDING]: new PrepareIdentityFundingHandler(walletRepository, identityFunding),
[MessagingMethods.GET_IDENTITY_FUNDING_SOURCES]: new GetIdentityFundingSourcesHandler(walletRepository, identityFunding),
[MessagingMethods.GET_IDENTITY_FUNDING_OPERATIONS]: new GetIdentityFundingOperationsHandler(identityFunding),
[MessagingMethods.CANCEL_IDENTITY_FUNDING]: new CancelIdentityFundingHandler(identityFunding),
[MessagingMethods.REGISTER_IDENTITY_FROM_CORE]: new RegisterIdentityFromCoreHandler(walletRepository, identityFunding),
[MessagingMethods.REGISTER_IDENTITY_FROM_PLATFORM_ADDRESS]: new RegisterIdentityFromPlatformAddressHandler(walletRepository, identityFunding),
[MessagingMethods.TOP_UP_IDENTITY_FROM_PLATFORM_ADDRESS]: new TopUpIdentityFromPlatformAddressHandler(walletRepository, identityFunding),
[MessagingMethods.REGISTER_IDENTITY_FROM_SHIELDED_POOL]: new RegisterIdentityFromShieldedPoolHandler(walletRepository, identityFunding),
[MessagingMethods.TOP_UP_IDENTITY_FROM_CORE]: new TopUpIdentityFromCoreHandler(walletRepository, identityFunding),
[MessagingMethods.GENERATE_PLATFORM_ADDRESSES]: new GeneratePlatformAddressesHandler(walletRepository, this.sdk),
[MessagingMethods.LIST_PLATFORM_ADDRESSES]: new ListPlatformAddressesHandler(walletRepository, this.sdk),
[MessagingMethods.GET_PLATFORM_ADDRESSES_INFOS]: new GetPlatformAddressesInfosHandler(this.sdk),
Expand Down
31 changes: 31 additions & 0 deletions src/content-script/api/fundingConflicts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { MessagingMethods } from '../../types/enums/MessagingMethods'
import { IdentityFundingOperation } from '../../types/IdentityFundingOperation'
import { isPendingFundingOperation } from '../repository/IdentityFundingRepository'

// Requests that draw on what a pending identity funding operation has reserved:
// its coins, its Platform address nonce, its shielded notes, or — for a legacy
// registration — the identity index sequence. Everything else may run alongside.
const CONFLICTS: Partial<Record<string, (operation: IdentityFundingOperation) => boolean>> = {
[MessagingMethods.REGISTER_IDENTITY]: operation => operation.kind === 'registration',
[MessagingMethods.SEND_PLATFORM_TRANSFER]: operation => operation.source === 'platform',
[MessagingMethods.WITHDRAW_PLATFORM_ADDRESS_TO_CORE]: operation => operation.source === 'platform',
[MessagingMethods.SHIELD_TO_POOL]: operation => operation.source === 'platform',
[MessagingMethods.SEND_SHIELDED_TRANSFER]: operation => operation.source === 'shielded',
[MessagingMethods.UNSHIELD_TO_ADDRESS]: operation => operation.source === 'shielded',
[MessagingMethods.WITHDRAW_SHIELDED_TO_CORE]: operation => operation.source === 'shielded'
}

// Whether a method can conflict with any funding operation at all, so the
// journal is read only when it matters.
export const canConflictWithFunding = (method: string): boolean => CONFLICTS[method] != null

// The pending operation a request would conflict with, if any.
export const findConflictingFunding = (method: string, operations: IdentityFundingOperation[]): IdentityFundingOperation | undefined => {
const conflicts = CONFLICTS[method]

if (conflicts == null) {
return undefined
}

return operations.find(operation => isPendingFundingOperation(operation) && conflicts(operation))
}
47 changes: 47 additions & 0 deletions src/content-script/api/private/identities/cancelIdentityFunding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { APIHandler } from '../../APIHandler'
import { EventData } from '../../../../types/EventData'
import { RepositoryScope } from '../../../../types/RepositoryScope'
import { IdentityFundingService } from '../../../services/IdentityFundingService'
import { validateFundingScope } from './identityFundingPayload'

type CancelIdentityFundingPayload = RepositoryScope & { operationId: string }

// Cancels a quote that was never sent, releasing its funds. Its reserved key and
// change indexes are not reused: they only ever move forward.
export class CancelIdentityFundingHandler implements APIHandler {
service: IdentityFundingService

constructor (service: IdentityFundingService) {
this.service = service
}

async handle (event: EventData): Promise<{ cancelled: boolean }> {
const payload: CancelIdentityFundingPayload = event.payload
const repository = this.service.repository(payload)

await repository.withLock(async () => {
const operation = await repository.get(payload.operationId)

if (operation == null || operation.status !== 'prepared') {
throw new Error('Only an unsubmitted quote can be cancelled')
}

await repository.save({ ...operation, status: 'cancelled' })
})

return { cancelled: true }
}

validatePayload (payload: CancelIdentityFundingPayload): string | null {
const scopeError = validateFundingScope(payload)

if (scopeError != null) {
return scopeError
}
if (typeof payload.operationId !== 'string') {
return 'Operation id must be provided'
}

return null
}
}
Loading
Loading