Skip to content
Open
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
8 changes: 6 additions & 2 deletions src/content-script/api/PrivateAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { SetSettingsHandler } from './private/settings/setSettings'
import { InitAccountXpubsHandler } from './private/wallet/initAccountXpubs'
import { GetCoreReceiveAddressHandler } from './private/core/getCoreReceiveAddress'
import { ListCoreAddressesHandler } from './private/core/listCoreAddresses'
import { ListCoreUtxosHandler } from './private/core/listCoreUtxos'
import { GetCoreBalanceHandler } from './private/core/getCoreBalance'
import { GetCoreTransactionsHandler } from './private/core/getCoreTransactions'
import { GeneratePlatformAddressesHandler } from './private/wallet/generatePlatformAddresses'
Expand Down Expand Up @@ -164,19 +165,22 @@ export class PrivateAPI {
assetLockFundingAddressesRepository,
this.storageAdapter,
this.sdk,
this.coreSDK
this.coreSDK,
coreExplorer
),
[MessagingMethods.TOP_UP_IDENTITY]: new TopUpIdentityHandler(
walletRepository,
identitiesRepository,
assetLockFundingAddressesRepository,
this.sdk,
this.coreSDK
this.coreSDK,
coreExplorer
),
[MessagingMethods.GET_SETTINGS]: new GetSettingsHandler(walletSettingsRepository),
[MessagingMethods.SET_SETTINGS]: new SetSettingsHandler(walletSettingsRepository),
[MessagingMethods.GET_CORE_RECEIVE_ADDRESS]: new GetCoreReceiveAddressHandler(walletRepository, coreExplorer, this.sdk),
[MessagingMethods.LIST_CORE_ADDRESSES]: new ListCoreAddressesHandler(walletRepository, coreExplorer, this.sdk),
[MessagingMethods.LIST_CORE_UTXOS]: new ListCoreUtxosHandler(walletRepository, coreExplorer),
[MessagingMethods.GET_CORE_BALANCE]: new GetCoreBalanceHandler(walletRepository, coreExplorer),
[MessagingMethods.GET_CORE_TRANSACTIONS]: new GetCoreTransactionsHandler(walletRepository, coreExplorer),
[MessagingMethods.GENERATE_PLATFORM_ADDRESSES]: new GeneratePlatformAddressesHandler(walletRepository, this.sdk),
Expand Down
51 changes: 51 additions & 0 deletions src/content-script/api/private/core/listCoreUtxos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { APIHandler } from '../../APIHandler'
import { WalletRepository } from '../../../repository/WalletRepository'
import { CoreExplorerService } from '../../../services/CoreExplorerService'
import { NetworkType } from '../../../../types/PlatformExplorer'
import { EmptyPayload } from '../../../../types/messages/payloads/EmptyPayload'
import { ListCoreUtxosResponse } from '../../../../types/messages/response/ListCoreUtxosResponse'

// The account's spendable Core outputs, read by xpub. An output listed here is
// what the caller names when funding an asset lock from the wallet's own coins.
export class ListCoreUtxosHandler implements APIHandler {
walletRepository: WalletRepository
coreExplorer: CoreExplorerService

constructor (walletRepository: WalletRepository, coreExplorer: CoreExplorerService) {
this.walletRepository = walletRepository
this.coreExplorer = coreExplorer
}

async handle (): Promise<ListCoreUtxosResponse> {
const wallet = await this.walletRepository.getCurrent()

if (wallet == null) {
throw new Error('No wallet is chosen')
}

const xpub = await this.walletRepository.getCoreAccountXpub(0)

if (xpub == null) {
return { utxos: [] }
}

const utxos = await this.coreExplorer.getXpubUtxos(xpub, wallet.network as NetworkType)

return {
utxos: utxos.map(utxo => ({
address: utxo.address,
txid: utxo.txid,
vout: utxo.vout,
amountDuffs: utxo.amount.toString()
}))
}
}

validatePayload (payload: EmptyPayload): string | null {
if ('account' in payload) {
return 'Account is not supported'
}

return null
}
}
98 changes: 65 additions & 33 deletions src/content-script/api/private/identities/registerIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { RegisterIdentityPayload } from '../../../../types/messages/payloads/Reg
import { RegisterIdentityResponse } from '../../../../types/messages/response/RegisterIdentityResponse'
import { IdentityType } from '../../../../types/enums/IdentityType'
import { buildAssetLockFromFundingTx } from '../../../../utils/buildAssetLockFromFundingTx'
import { deriveCoreAccountXpub, deriveCoreAddressPrivateKey } from '../../../../utils/coreAddresses'
import { CoreExplorerService } from '../../../services/CoreExplorerService'
import { NetworkType } from '../../../../types/PlatformExplorer'
import { waitForAssetLockProof } from '../../../../utils/waitForAssetLockProof'
import { IDENTITY_KEY_DEFINITIONS, buildIdentityCreateTransition } from '../../../../utils/identityRegistration'
import {
Expand All @@ -35,21 +38,24 @@ export class RegisterIdentityHandler implements APIHandler {
storageAdapter: StorageAdapter
sdk: DashPlatformSDK
coreSDK: DashCoreSDK
coreExplorer: CoreExplorerService

constructor (
walletRepository: WalletRepository,
identitiesRepository: IdentitiesRepository,
assetLockFundingAddressesRepository: AssetLockFundingAddressesRepository,
storageAdapter: StorageAdapter,
sdk: DashPlatformSDK,
coreSDK: DashCoreSDK
coreSDK: DashCoreSDK,
coreExplorer: CoreExplorerService
) {
this.walletRepository = walletRepository
this.identitiesRepository = identitiesRepository
this.assetLockFundingAddressesRepository = assetLockFundingAddressesRepository
this.storageAdapter = storageAdapter
this.sdk = sdk
this.coreSDK = coreSDK
this.coreExplorer = coreExplorer
}

async handle (event: EventData): Promise<RegisterIdentityResponse> {
Expand All @@ -67,34 +73,42 @@ export class RegisterIdentityHandler implements APIHandler {
}

// ── 2. Load the asset lock funding address entry ────────────────────────
// No record means the address is not a deposit this extension handed out,
// but one of the wallet's own: it is funded from its own coins, and L1 keeps
// the only record of what an earlier attempt committed.
const assetLockFundingAddressEntry = await this.assetLockFundingAddressesRepository.getByAddress(payload.assetLockFundingAddress)

if (assetLockFundingAddressEntry == null) {
throw new Error(
`Asset lock funding address ${payload.assetLockFundingAddress} not found. ` +
'It may belong to a different wallet or network.'
)
}

if (assetLockFundingAddressEntry.used) {
if (assetLockFundingAddressEntry?.used === true) {
throw new Error(`Asset lock funding address ${payload.assetLockFundingAddress} has already been used for registration`)
}

// ── 3. Decrypt the one-time funding key ─────────────────────────────────
// The funding key signs the asset lock tx inputs only. Credit output
// ownership and Platform ST signing are handled by identityRegistrationKey
// derived in step 5 (DIP-0013).
const passwordHash = hash.sha256().update(payload.password).digest('hex')
const secretKey = PrivateKey.fromHex(passwordHash)
let assetLockFundingPrivateKey: PrivateKeyWASM

let assetLockFundingKeyBytes: Uint8Array
try {
assetLockFundingKeyBytes = decrypt(secretKey.toHex(), hexToBytes(assetLockFundingAddressEntry.encryptedPrivateKey))
} catch {
throw new Error('Failed to decrypt asset lock funding key — wrong password or corrupted entry')
}
if (assetLockFundingAddressEntry == null) {
const xpub = await this.walletRepository.getCoreAccountXpub(0) ??
await deriveCoreAccountXpub(wallet, payload.password, 0, this.sdk)
const { nextUnused } = await this.coreExplorer.getXpubSummary(xpub, wallet.network as NetworkType)

assetLockFundingPrivateKey = await deriveCoreAddressPrivateKey(
wallet, payload.password, xpub, payload.assetLockFundingAddress, nextUnused, this.sdk
)
} else {
const passwordHash = hash.sha256().update(payload.password).digest('hex')
const secretKey = PrivateKey.fromHex(passwordHash)

const assetLockFundingPrivateKey = PrivateKeyWASM.fromBytes(assetLockFundingKeyBytes, wallet.network)
let assetLockFundingKeyBytes: Uint8Array
try {
assetLockFundingKeyBytes = decrypt(secretKey.toHex(), hexToBytes(assetLockFundingAddressEntry.encryptedPrivateKey))
} catch {
throw new Error('Failed to decrypt asset lock funding key — wrong password or corrupted entry')
}

assetLockFundingPrivateKey = PrivateKeyWASM.fromBytes(assetLockFundingKeyBytes, wallet.network)
}

// ── 4. Determine the identity index ─────────────────────────────────────
// The credit output address — and therefore the asset lock txid — is derived
Expand All @@ -105,10 +119,16 @@ export class RegisterIdentityHandler implements APIHandler {
// rebuild a different tx than the one already committed on L1.
let identityIndex: number

if (assetLockFundingAddressEntry.assetLockTxid == null) {
// Without a record, the funding output itself says whether an earlier
// attempt already committed an asset lock, and which one.
const committedAssetLockTxid = assetLockFundingAddressEntry == null
? await this.coreExplorer.getOutputSpender(payload.assetLockFundingTxid, payload.assetLockFundingAddress, wallet.network as NetworkType)
: assetLockFundingAddressEntry.assetLockTxid ?? null

if (committedAssetLockTxid == null) {
// Fresh registration: scan for the next free index on-chain.
identityIndex = await this.scanFreeIdentityIndex(wallet, payload.password)
} else if (assetLockFundingAddressEntry.registrationIdentityIndex != null) {
} else if (assetLockFundingAddressEntry?.registrationIdentityIndex != null) {
// Recovery: reuse the index pinned when the asset lock was broadcast.
identityIndex = assetLockFundingAddressEntry.registrationIdentityIndex
} else {
Expand All @@ -119,7 +139,7 @@ export class RegisterIdentityHandler implements APIHandler {
wallet,
payload,
assetLockFundingPrivateKey,
assetLockFundingAddressEntry.assetLockTxid
committedAssetLockTxid
)
}

Expand All @@ -142,13 +162,10 @@ export class RegisterIdentityHandler implements APIHandler {

const assetLockTxid = assetLockTx.hash()

if (
assetLockFundingAddressEntry.assetLockTxid != null &&
assetLockFundingAddressEntry.assetLockTxid !== assetLockTxid
) {
if (committedAssetLockTxid != null && committedAssetLockTxid !== assetLockTxid) {
throw new Error(
`Asset lock funding address ${payload.assetLockFundingAddress} is already broadcasted ` +
`with a different asset lock txid (${assetLockFundingAddressEntry.assetLockTxid})`
`with a different asset lock txid (${committedAssetLockTxid})`
)
}

Expand All @@ -161,12 +178,24 @@ export class RegisterIdentityHandler implements APIHandler {
[txidToFilterBytes(assetLockTxid)]
)

if (assetLockFundingAddressEntry.assetLockTxid == null) {
await this.coreSDK.broadcastTransaction(assetLockTx.bytes())
// Persist the broadcasted txid AND the identity index before any further
// work, so a retry after a crash rebuilds the exact same asset lock instead
// of re-scanning to a different index.
await this.assetLockFundingAddressesRepository.markAsBroadcasted(payload.assetLockFundingAddress, assetLockTxid, identityIndex)
if (committedAssetLockTxid == null) {
try {
await this.coreSDK.broadcastTransaction(assetLockTx.bytes())
} catch (e) {
// The explorer lags the mempool, so an asset lock broadcast moments ago
// still reads as unspent. Rebuilt byte for byte, it is the same
// transaction, and the network rejecting it as known is not a failure.
if (await this.coreSDK.getTransaction(assetLockTxid).catch(() => null) == null) {
throw e
}
}

if (assetLockFundingAddressEntry != null) {
// Persist the broadcasted txid AND the identity index before any further
// work, so a retry after a crash rebuilds the exact same asset lock instead
// of re-scanning to a different index.
await this.assetLockFundingAddressesRepository.markAsBroadcasted(payload.assetLockFundingAddress, assetLockTxid, identityIndex)
}
}

// ── 8. Wait for instant lock or chain lock (whichever comes first) ──────
Expand Down Expand Up @@ -259,7 +288,10 @@ export class RegisterIdentityHandler implements APIHandler {
}

// ── 15. Mark funding address as used and switch identity ────────────────
await this.assetLockFundingAddressesRepository.markAsUsed(payload.assetLockFundingAddress)
if (assetLockFundingAddressEntry != null) {
await this.assetLockFundingAddressesRepository.markAsUsed(payload.assetLockFundingAddress)
}

await this.walletRepository.switchIdentity(identifier)

return {
Expand Down
Loading
Loading