Skip to content
Merged
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
531 changes: 270 additions & 261 deletions CLAUDE.md

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions src/main/migrations/0017_platform_addresses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type {Knex} from 'knex'

// Platform (L2) addresses become rows, like L1 ones, instead of a count on the
// wallet. A row carries the address's own used flag and stops the whole list
// being re-derived from the xpub on every read.
//
// No backfill here: the addresses derive from platform_xpub, which needs the
// SDK. PlatformAddressService seeds the rows on its first window run and reads
// wallet.platform_address_count once as the floor, so a manually revealed
// address survives.

export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable('platform_addresses', table => {
table.string('wallet_id').notNullable()
table.integer('address_index').notNullable()
table.text('address').notNullable()
table.text('derivation_path').notNullable()
table.boolean('is_used').notNullable().defaultTo(false)
table.primary(['wallet_id', 'address_index'])
})
}

export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists('platform_addresses')
}
21 changes: 21 additions & 0 deletions src/main/migrations/0018_shielded_address_rows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type {Knex} from 'knex'

// Lets shielded addresses run the shared gap walk, which needs a per-address used
// flag. No derivation_path column: a diversifier is not a path element, so the
// path would be m/32'/coinType'/account' on every row — the account is already
// implied by the wallet and the diversifier is address_index.
//
// Existing rows are unused as far as anything knows; the first walk after a sync
// sets the flag from the decrypted notes.

export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable('shielded_addresses', table => {
table.boolean('is_used').notNullable().defaultTo(false)
})
}

export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('shielded_addresses', table => {
table.dropColumn('is_used')
})
}
2 changes: 1 addition & 1 deletion src/main/platform/operations/address/signInputs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {DashPlatformSDK} from 'dash-platform-sdk'
import {AddressWitnessWASM, InputAddressWASM, AddressFundsFeeStrategyStepWASM} from 'dash-platform-sdk/types.js'
import {Network} from '../../../src/types/Network'
import {PLATFORM_ACCOUNT} from '../../../src/constants'
import {PLATFORM_ACCOUNT} from '../../../src/constants/addresses'
import {AddressInput} from '../../types/messages'

// Fees come out of the first input for every address-funded transition.
Expand Down
2 changes: 1 addition & 1 deletion src/main/platform/operations/assetLock/identityCreate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {PlatformOperations} from '../../types/messages'
import {OperationContext, OperationError} from '../types'
import {broadcast} from '../broadcast'
import {assetLockProofParams} from '../assetLockProof'
import {IDENTITY_KEY_DEFINITIONS} from '../../../src/constants'
import {IDENTITY_KEY_DEFINITIONS} from '../../../src/constants/credits'

type Payload = PlatformOperations['identityCreateFromAssetLock']['payload']
type Result = PlatformOperations['identityCreateFromAssetLock']['result']
Expand Down
2 changes: 1 addition & 1 deletion src/main/platform/operations/assetLock/shield.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {OperationContext, OperationError} from '../types'
import {broadcast} from '../broadcast'
import {buildAssetLockProof} from '../assetLockProof'
import {SHIELD_FUNDING_DUMMY_OUTPUTS} from '../shielded/constants'
import {SHIELDED_ACCOUNT} from '../../../src/constants'
import {SHIELDED_ACCOUNT} from '../../../src/constants/addresses'

type Payload = PlatformOperations['shieldFromAssetLock']['payload']
type Result = PlatformOperations['shieldFromAssetLock']['result']
Expand Down
2 changes: 1 addition & 1 deletion src/main/platform/operations/fee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {buildAssetLockProof} from './assetLockProof'
import {DEDUCT_FROM_FIRST} from './address/signInputs'
import {minimumFee} from './shielded/spend/fee'
import {MAX_SPEND_NOTES, MIN_BUNDLE_ACTIONS} from './shielded/constants'
import {IDENTITY_KEY_DEFINITIONS, SHIELD_FUNDING_FEE_RESERVE_CREDITS} from '../../src/constants'
import {IDENTITY_KEY_DEFINITIONS, SHIELD_FUNDING_FEE_RESERVE_CREDITS} from '../../src/constants/credits'

type Payload = PlatformOperations['transitionFee']['payload']
type Result = PlatformOperations['transitionFee']['result']
Expand Down
4 changes: 2 additions & 2 deletions src/main/platform/operations/shielded/checkSpent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ export async function checkSpent(
const statuses = await sdk.shielded.getShieldedNullifiers(recovered.map(note => note.nullifier))
const byNullifier = new Map(statuses.map(status => [hex(status.nullifier), status.isSpent]))

return recovered.map(note => ({note, spent: byNullifier.get(hex(note.nullifier)) === true}))
}
return recovered.map(recoveredNote => ({recoveredNote, spent: byNullifier.get(hex(recoveredNote.nullifier)) === true}))
}
2 changes: 1 addition & 1 deletion src/main/platform/operations/shielded/shield.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {OrchardAddressWASM, ShieldedMemoWASM} from 'pshenmic-dpp'
import {PlatformOperations} from '../../types/messages'
import {OperationContext} from '../types'
import {broadcast} from '../broadcast'
import {PLATFORM_ACCOUNT, SHIELDED_ACCOUNT} from '../../../src/constants'
import {PLATFORM_ACCOUNT, SHIELDED_ACCOUNT} from '../../../src/constants/addresses'

type Payload = PlatformOperations['shield']['payload']
type Result = PlatformOperations['shield']['result']
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {OrchardAddressWASM, ShieldedMemoWASM, SpendableNoteWASM, StateTransition
import {Network} from '../../../../src/types/Network'
import {coreAddressToScript} from '../../../../src/utils/coreScript'
import {PlatformOperations} from '../../../types/messages'
import {COIN_TYPE, SHIELDED_ACCOUNT} from '../../../../src/constants'
import {COIN_TYPE, SHIELDED_ACCOUNT} from '../../../../src/constants/addresses'
import {identityKeys} from './identityKeys'

type Payload = PlatformOperations['spend']['payload']
Expand Down
2 changes: 1 addition & 1 deletion src/main/platform/operations/shielded/spend/fee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
UnshieldTransitionWASM,
} from 'pshenmic-dpp'
import {PoolSpendOperation} from '../../../types/messages'
import {IDENTITY_KEY_DEFINITIONS} from '../../../../src/constants'
import {IDENTITY_KEY_DEFINITIONS} from '../../../../src/constants/credits'
import {MIN_BUNDLE_ACTIONS} from '../constants'
// What consensus will charge, from the protocol implementation itself. Never
// reimplement this: it is versioned (`platformVersion`) and scales with the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {DashPlatformSDK} from 'dash-platform-sdk'
import {IdentityPublicKeyInCreation} from 'dash-platform-sdk/types.js'
import {PrivateKeyWASM} from 'pshenmic-dpp'
import {Network} from '../../../../src/types/Network'
import {IDENTITY_KEY_DEFINITIONS} from '../../../../src/constants'
import {IDENTITY_KEY_DEFINITIONS} from '../../../../src/constants/credits'
// createStateTransition destructures plain {id, purpose, ...} objects and
// builds the WASM keys itself — passing IdentityPublicKeyInCreationWASM here
// breaks (its field is keyId).
Expand Down
6 changes: 3 additions & 3 deletions src/main/platform/operations/shielded/spend/spend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {buildTransition} from './buildTransition'
import {checkSpent} from '../checkSpent'
import {actualFee, minimumFee} from './fee'
import {MAX_SPEND_NOTES} from '../constants'
import {SHIELDED_ACCOUNT} from '../../../../src/constants'
import {SHIELDED_ACCOUNT} from '../../../../src/constants/addresses'
import {waitForResult} from './waitForResult'

type Payload = PlatformOperations['spend']['payload']
Expand All @@ -26,10 +26,10 @@ export async function spend(payload: Payload, ctx: OperationContext): Promise<Re
// Before proving, not after: a proof costs seconds and the nullifier query
// one round trip.
const checked = await checkSpent(sdk, recovered)
const stale = checked.filter(({spent}) => spent).map(({note}) => note.index)
const stale = checked.filter(({spent}) => spent).map(({recoveredNote}) => recoveredNote.index)
if (stale.length > 0) ctx.notesSpent(stale)

const unspent = checked.filter(({spent}) => !spent).map(({note}) => note)
const unspent = checked.filter(({spent}) => !spent).map(({recoveredNote}) => recoveredNote)
const available = payload.noteIndexes != null
? unspent.filter(note => payload.noteIndexes!.includes(note.index))
: unspent
Expand Down
14 changes: 7 additions & 7 deletions src/main/platform/operations/shielded/sync.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {PlatformOperations} from '../../types/messages'
import {OperationContext, throwIfAborted} from '../types'
import {checkSpent} from './checkSpent'
import {SHIELDED_ACCOUNT} from '../../../src/constants'
import {SHIELDED_ACCOUNT} from '../../../src/constants/addresses'

type Payload = PlatformOperations['sync']['payload']
type Result = PlatformOperations['sync']['result']
Expand All @@ -16,17 +16,17 @@ export async function sync(payload: Payload, ctx: OperationContext): Promise<Res
const checked = await checkSpent(ctx.sdk, recovered)

let balance = 0n
const notes = checked.map(({note, spent}) => {
if (!spent) balance += note.note.value
const notes = checked.map(({recoveredNote, spent}) => {
if (!spent) balance += recoveredNote.note.value
return {
// recoverNotes indexes by array position; map it back to the pool index.
index: all[note.index]?.index ?? note.index,
amount: note.note.value,
index: all[recoveredNote.index]?.index ?? recoveredNote.index,
amount: recoveredNote.note.value,
spent,
address: note.note.address.toBech32m(ctx.network),
address: recoveredNote.note.address.toBech32m(ctx.network),
}
})
notes.sort((a, b) => b.index - a.index)

return {balance, notes}
}
}
2 changes: 1 addition & 1 deletion src/main/platform/types/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ export interface InFlight {
}

export interface CheckedNote {
note: RecoveredNoteWASM
recoveredNote: RecoveredNoteWASM
spent: boolean
}
35 changes: 20 additions & 15 deletions src/main/src/WalletBackend.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import {calibratePBKDF2Iterations, getKnex, migrateKnex} from './utils'
import {dataPath, ensureDataFolder} from './utils/dataPath'
import {LogsFolderName, PBKDF2_TARGET_MS, PreferencesFilename, SHIELDED_NOTES_CHECK_INTERVAL_MS, StorageFilename} from './constants'
import {LogsFolderName, PBKDF2_TARGET_MS, PreferencesFilename, StorageFilename} from './constants/app'
import {SHIELDED_NOTES_CHECK_INTERVAL_MS} from './constants/credits'
import { ipcMain } from 'electron'
import { WalletDAO } from './database/WalletDAO'
import { AddressDAO } from './database/AddressDAO'
import { PlatformAddressDAO } from './database/PlatformAddressDAO'
import { IdentityDAO } from './database/IdentityDAO'
import { TransactionDAO } from './database/TransactionDAO'
import { ContactDAO } from './database/ContactDAO'
import { WalletService } from './services/wallet/WalletService'
import { IdentityRegistrationService } from './services/platform/IdentityRegistrationService'
import { PlatformAddressService } from './services/platform/PlatformAddressService'
import { PlatformTransferService } from './services/platform/PlatformTransferService'
import { ApplicationService } from './services/app/ApplicationService'
import {Preferences} from "./preferences";
import { CreateWalletHandler } from './api/wallet/createWallet'
Expand Down Expand Up @@ -89,7 +92,7 @@ import {StartWalletSyncHandler} from './api/walletSync/startWalletSync'
import {StopWalletSyncHandler} from './api/walletSync/stopWalletSync'
import {ResetWalletSyncHandler} from './api/walletSync/resetWalletSync'
import {GetUtxosHandler} from './api/walletSync/getUtxos'
import {DISCOVERY_INTERVAL_MS} from './constants'
import {DISCOVERY_INTERVAL_MS} from './constants/addresses'
import {CoreDiscoveryService} from './services/core/CoreDiscoveryService'
import {CorePrevOutService} from './services/core/CorePrevOutService'
import {WalletCredentialsService} from './services/wallet/WalletCredentialsService'
Expand All @@ -108,6 +111,7 @@ import {ShowLogFileInFolderHandler} from './api/logs/showLogFileInFolder'
export class WalletBackend {
private walletService?: WalletService
private platformAddressService?: PlatformAddressService
private platformTransferService?: PlatformTransferService
private feeService?: FeeService
private applicationService?: ApplicationService
private walletSyncService?: WalletSyncService
Expand All @@ -128,7 +132,7 @@ export class WalletBackend {
private identityDAO?: IdentityDAO

private initHandlers(): void {
if (!this.walletService || !this.platformAddressService || !this.feeService || !this.applicationService || !this.walletSyncService || !this.ratesService || !this.contactService || !this.shieldedService || !this.assetLockService || !this.addressDAO || !this.walletDAO || !this.identityDAO || !this.identityRegistrationService || !this.coreDiscoveryService || !this.coreLockService || !this.walletCredentialsService || !this.identityService || !this.logService) {
if (!this.walletService || !this.platformAddressService || !this.platformTransferService || !this.feeService || !this.applicationService || !this.walletSyncService || !this.ratesService || !this.contactService || !this.shieldedService || !this.assetLockService || !this.addressDAO || !this.walletDAO || !this.identityDAO || !this.identityRegistrationService || !this.coreDiscoveryService || !this.coreLockService || !this.walletCredentialsService || !this.identityService || !this.logService) {
throw new Error('Services not initialized. Call start() first.')
}

Expand All @@ -154,18 +158,18 @@ export class WalletBackend {
ipcMain.handle('sendTransaction', new SendTransactionHandler(this.walletService).handle)
ipcMain.handle('getTxLockStatus', new GetTxLockStatusHandler(this.coreLockService).handle)
ipcMain.handle('estimateFee', new EstimateFeeHandler(this.feeService).handle)
ipcMain.handle('sendPlatformTransfer', new SendPlatformTransferHandler(this.platformAddressService).handle)
ipcMain.handle('topUpIdentityFromAddresses', new TopUpIdentityFromAddressesHandler(this.platformAddressService).handle)
ipcMain.handle('withdrawPlatformCredits', new WithdrawPlatformCreditsHandler(this.platformAddressService).handle)
ipcMain.handle('sendIdentityCredits', new SendIdentityCreditsHandler(this.platformAddressService).handle)
ipcMain.handle('transferIdentityCredits', new TransferIdentityCreditsHandler(this.platformAddressService).handle)
ipcMain.handle('withdrawIdentityCredits', new WithdrawIdentityCreditsHandler(this.platformAddressService).handle)
ipcMain.handle('createIdentityFromAddresses', new CreateIdentityFromAddressesHandler(this.platformAddressService).handle)
ipcMain.handle('startAssetLockFunding', new StartAssetLockFundingHandler(this.platformAddressService, this.shieldedService, this.identityRegistrationService).handle)
ipcMain.handle('sendPlatformTransfer', new SendPlatformTransferHandler(this.platformTransferService).handle)
ipcMain.handle('topUpIdentityFromAddresses', new TopUpIdentityFromAddressesHandler(this.platformTransferService).handle)
ipcMain.handle('withdrawPlatformCredits', new WithdrawPlatformCreditsHandler(this.platformTransferService).handle)
ipcMain.handle('sendIdentityCredits', new SendIdentityCreditsHandler(this.platformTransferService).handle)
ipcMain.handle('transferIdentityCredits', new TransferIdentityCreditsHandler(this.platformTransferService).handle)
ipcMain.handle('withdrawIdentityCredits', new WithdrawIdentityCreditsHandler(this.platformTransferService).handle)
ipcMain.handle('createIdentityFromAddresses', new CreateIdentityFromAddressesHandler(this.platformTransferService).handle)
ipcMain.handle('startAssetLockFunding', new StartAssetLockFundingHandler(this.platformTransferService, this.shieldedService, this.identityRegistrationService).handle)
ipcMain.handle('getAssetLockFundingState', new GetAssetLockFundingStateHandler(this.assetLockService).handle)
ipcMain.handle('resumeAssetLockFunding', new ResumeAssetLockFundingHandler(this.assetLockService, this.platformAddressService, this.shieldedService, this.identityRegistrationService).handle)
ipcMain.handle('resumeAssetLockFunding', new ResumeAssetLockFundingHandler(this.assetLockService, this.platformTransferService, this.shieldedService, this.identityRegistrationService).handle)
ipcMain.handle('dismissAssetLockFunding', new DismissAssetLockFundingHandler(this.assetLockService).handle)
ipcMain.handle('shieldToPool', new ShieldToPoolHandler(this.platformAddressService).handle)
ipcMain.handle('shieldToPool', new ShieldToPoolHandler(this.platformTransferService).handle)
ipcMain.handle('verifyWalletPassword', new VerifyWalletPasswordHandler(this.walletCredentialsService).handle)
ipcMain.handle('exportMnemonic', new ExportMnemonicHandler(this.walletCredentialsService).handle)
ipcMain.handle('verifyWalletMnemonic', new VerifyWalletMnemonicHandler(this.walletCredentialsService).handle)
Expand Down Expand Up @@ -245,9 +249,10 @@ export class WalletBackend {
this.walletService = new WalletService(walletDAO, addressDAO, identityDAO, this.identityService, this.walletSyncService, this.platformWorkerService, providers, this.coreDiscoveryService, coreTransactionService, preferences, calibratedIterations)
this.assetLockService = new AssetLockService(walletDAO, new AssetLockDAO(knex), this.coreLockService, this.platformWorkerService)
this.shieldedService = new ShieldedService(walletDAO, identityDAO, new ShieldedNoteDAO(knex), new ShieldedPoolDAO(knex), shieldedAddressDAO, this.platformWorkerService, this.assetLockService, preferences)
this.feeService = new FeeService(walletDAO, this.platformWorkerService, this.shieldedService, preferences)
this.platformAddressService = new PlatformAddressService(walletDAO, new PlatformAddressDAO(knex), this.platformWorkerService)
this.feeService = new FeeService(walletDAO, this.platformAddressService, this.platformWorkerService, this.shieldedService, preferences)
this.identityRegistrationService = new IdentityRegistrationService(walletDAO, identityDAO, this.assetLockService, this.platformWorkerService, this.coreLockService, this.feeService)
this.platformAddressService = new PlatformAddressService(walletDAO, identityDAO, this.assetLockService, this.platformWorkerService, this.shieldedService, this.feeService, preferences)
this.platformTransferService = new PlatformTransferService(walletDAO, identityDAO, this.assetLockService, this.platformAddressService, this.platformWorkerService, this.shieldedService, this.feeService, preferences)
this.walletDAO = walletDAO
this.addressDAO = addressDAO
this.identityDAO = identityDAO
Expand Down
4 changes: 2 additions & 2 deletions src/main/src/api/wallet/addWalletAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export class AddWalletAddressHandler {
this.walletService = walletService
}

handle = async (_event: IpcMainInvokeEvent, walletId: string, password: string, isChange: boolean): Promise<string> => {
return this.walletService.addAddress(walletId, password, isChange)
handle = async (_event: IpcMainInvokeEvent, walletId: string, isChange: boolean): Promise<string> => {
return this.walletService.addAddress(walletId, isChange)
}
}
10 changes: 5 additions & 5 deletions src/main/src/api/wallet/createIdentityFromAddresses.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { IpcMainInvokeEvent } from 'electron/utility'
import { PlatformAddressService } from '../../services/platform/PlatformAddressService'
import { PlatformTransferService } from '../../services/platform/PlatformTransferService'
import { IdentityCreateResult } from '../../types/IdentityCreateResult'

export class CreateIdentityFromAddressesHandler {
private platformAddressService: PlatformAddressService
private platformTransferService: PlatformTransferService

constructor(platformAddressService: PlatformAddressService) {
this.platformAddressService = platformAddressService
constructor(platformTransferService: PlatformTransferService) {
this.platformTransferService = platformTransferService
}

handle = async (
Expand All @@ -16,6 +16,6 @@ export class CreateIdentityFromAddressesHandler {
amountCredits: bigint,
password: string,
): Promise<IdentityCreateResult> => {
return this.platformAddressService.createIdentityFromAddresses(walletId, fromAddress, amountCredits, password)
return this.platformTransferService.createIdentityFromAddresses(walletId, fromAddress, amountCredits, password)
}
}
Loading
Loading