import { Address, BNExtended, BufferWriter, Script, ScriptOpcode } from "libnexa-ts"; import type { VaultDTO } from "../types"; import { AccountType, generateVaultAddress, KeySpace } from "../utils"; import type { KeyManager } from "./key"; import type { RostrumService } from "./rostrum"; const HODL_FIRST_BLOCK = 274710; const HODL_SCRIPT_PREFIX = "0014461ad25081cb0119d034385ff154c8d3ad6bdd76"; export class WalletDiscoveryService { private readonly rostrumService: RostrumService; private readonly keyManager: KeyManager; public constructor(rostrumService: RostrumService, keyManager: KeyManager) { this.rostrumService = rostrumService; this.keyManager = keyManager; } public async discoverWalletIndex(type: AccountType, keySpace: KeySpace): Promise { let index = 0, stop = false, addrBatch = 0; do { stop = true; for (let i = addrBatch; i < addrBatch+20; i++) { const keyPath = { account: type, type: keySpace, index: i }; const rAddr = this.keyManager.getKey(keyPath).privateKey.toAddress().toString(); const isUsed = await this.rostrumService.isAddressUsed(rAddr); if (isUsed) { index = i; stop = false; } } addrBatch += 20; } while (!stop); return index; } public async discoverVaults(addresses: string[]): Promise> { const vaultsPromises: Promise>[] = []; for (const address of addresses) { const p = this.checkVaultsForAddress(address); vaultsPromises.push(p); } const res = await Promise.all(vaultsPromises); const vaults = new Map(); res.forEach(set => { set.forEach(hex => { const vault = this.parseVaultDetails(hex); if (vault) { vaults.set(vault.address, vault); } }); }); return vaults; } private async checkVaultsForAddress(address: string): Promise> { const vaults = new Set(); const history = await this.rostrumService.getTransactionsHistory(address, HODL_FIRST_BLOCK); for (const txHistory of history) { const tx = await this.rostrumService.getTransaction(txHistory.tx_hash); const hodls = tx.vout.filter(out => out.scriptPubKey.hex.startsWith(HODL_SCRIPT_PREFIX)); for (const hodl of hodls) { vaults.add(hodl.scriptPubKey.hex); } } return vaults; } private parseVaultDetails(hex: string): VaultDTO | undefined { const scirptTemplate = Script.fromHex(hex); const buf = new BufferWriter().writeVarLengthBuf(scirptTemplate.toBuffer()).toBuffer(); const actualAddress = new Address(buf).toString(); const args = scirptTemplate.getVisibleArgs(); const block = BNExtended.fromScriptNumBuffer(args.chunks[0].buf!).toNumber(); const index = ScriptOpcode.isSmallIntOp(args.chunks[1].opcodenum) ? ScriptOpcode.decodeOP_N(args.chunks[1].opcodenum) : BNExtended.fromScriptNumBuffer(args.chunks[1].buf!).toNumber(); const visibleArgs = [block, index]; const key = this.keyManager.getKey({ account: AccountType.VAULT, type: KeySpace.RECEIVE, index: index }); const expectedAddress = generateVaultAddress(key.publicKey, visibleArgs); if (actualAddress != expectedAddress) { return undefined; } const dto: VaultDTO = { address: actualAddress, block: block, idx: index, statusHash: '', balance: { confirmed: "0", unconfirmed: "0" }, tokensBalance: {}, height: 0, type: AccountType.VAULT } return dto; } }