From 1bac9ff32f663b366f8426b69397966616b93f4d Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Wed, 3 Jun 2026 07:09:51 -0700 Subject: [PATCH] Fixing issue with indexing --- dev-docs/psf-memo-db.md | 23 ++++++++++++ src/lib/memo-parser.js | 58 ++++++++++++++++++++++++++----- test/unit/lib/memo-parser.unit.js | 49 +++++++++++++++++++++++++- 3 files changed, 121 insertions(+), 9 deletions(-) diff --git a/dev-docs/psf-memo-db.md b/dev-docs/psf-memo-db.md index 750e52b..278d007 100644 --- a/dev-docs/psf-memo-db.md +++ b/dev-docs/psf-memo-db.md @@ -81,6 +81,29 @@ All routes are under `/level` with a consistent CRUD pattern generated from `ENT | `processerror` | `txid` | `errorData` | txid | | `ptx` | `txid` | `ptxData` | txid | +### Read routes (query use cases) + +| Method | Path | Query params | +|--------|------|----------------| +| `GET` | `/profile/recent` | `limit` (default 100, max 100), `offset` (default 0) | + +Returns profiles sorted by **block height** (newest first), using each profile’s `txid` to look up `blockHeight` in `ptxs`. Tie-breaker: `seen` timestamp descending. + +Response shape: + +```json +{ + "profiles": [ + { "addr": "bitcoincash:q...", "text": "...", "txid": "...", "seen": 123, "blockHeight": 600000 } + ], + "pagination": { "limit": 100, "offset": 0, "total": 42, "hasMore": false } +} +``` + +Implementation: `profile-query` adapter (LevelDB scan) → `list-recent-profiles` use case → `/profile` REST controller. + +**Tradeoff:** Full scan of `profiles` on each request; suitable for moderate corpus sizes. A height-indexed store would be needed for very large archives. + ### Status (special case) Matches SLP status semantics: diff --git a/src/lib/memo-parser.js b/src/lib/memo-parser.js index db4b92e..6757830 100644 --- a/src/lib/memo-parser.js +++ b/src/lib/memo-parser.js @@ -111,19 +111,61 @@ export function findMemoOutputs (txDetails) { return matches } +/** + * Extract compressed or uncompressed pubkey from a P2PKH unlocking script. + * Mirrors Go wallet.GetP2pkhAddrFromUnlockScript (pubkey push after signature). + */ +export function getPubkeyFromUnlockScript (scriptBuf) { + if (!scriptBuf || !scriptBuf.length) return null + + let decoded + try { + decoded = getBchjs().Script.decode(scriptBuf) + } catch (err) { + return null + } + + if (!decoded || !decoded.length) return null + + for (let i = decoded.length - 1; i >= 0; i--) { + const chunk = decoded[i] + if (Buffer.isBuffer(chunk) && (chunk.length === 33 || chunk.length === 65)) { + return chunk + } + } + + return null +} + +/** + * Derive cash address from unlocking script hex (P2PKH spend). + */ +export function getAddressFromUnlockScriptHex (scriptHex) { + if (!scriptHex || typeof scriptHex !== 'string') return null + + try { + const scriptBuf = Buffer.from(scriptHex, 'hex') + const pubkey = getPubkeyFromUnlockScript(scriptBuf) + if (!pubkey) return null + + const hash160 = getBchjs().Crypto.hash160(pubkey) + return getBchjs().Address.hash160ToCash(hash160) + } catch (err) { + return null + } +} + export function getSignerAddress (txDetails) { if (!txDetails || !txDetails.vin) return null for (const vin of txDetails.vin) { - if (!vin.scriptSig || !vin.scriptSig.hex) continue - try { - const scriptBuf = Buffer.from(vin.scriptSig.hex, 'hex') - const addr = getBchjs().Script.getAddressFromScriptSig(scriptBuf) - if (addr) return addr - } catch (err) { - continue - } + const scriptHex = vin.scriptSig && vin.scriptSig.hex + if (!scriptHex) continue + + const addr = getAddressFromUnlockScriptHex(scriptHex) + if (addr) return addr } + return null } diff --git a/test/unit/lib/memo-parser.unit.js b/test/unit/lib/memo-parser.unit.js index 98ad416..948613e 100644 --- a/test/unit/lib/memo-parser.unit.js +++ b/test/unit/lib/memo-parser.unit.js @@ -2,7 +2,9 @@ import { assert } from 'chai' import { decodeMemoOpReturn, parseScriptPushDatas, - isMemoScript + isMemoScript, + getSignerAddress, + getAddressFromUnlockScriptHex } from '../../../src/lib/memo-parser.js' import { CODE_POST, CODE_PREFIX } from '../../../src/lib/memo-codes.js' @@ -38,4 +40,49 @@ describe('#memo-parser', () => { assert.equal(isMemoScript(POST_OP_RETURN_HEX), true) }) }) + + describe('#getAddressFromUnlockScriptHex', () => { + const SCRIPT_SIG_1 = + '483045022100e15e18048b78e744fe8e440eb2687b5d4eb7009140d5e71950d4351c6e3bf2ef022031b0dab7326846a58e16f6a753e0d011514f827d2adacde63912daa3f85636c5412103833927b98fab40d5f857214d802ba42ea21087105b81e9bff57bf2486c9ff171' + const SCRIPT_SIG_2 = + '483045022100aa35ee315859b63f92e7878304364635943b28b2334dfd796a51cb2e0b284fe3022033e852b7c965f36c7d6eecc577b01218d611450c80e75af1fd41e76cfc7af81c4121035c5970c98aa4543271348bb18de4fb04122af87555fc4411c376493cc85594c3' + + it('should derive cash address from real memo tx scriptSig', () => { + const addr = getAddressFromUnlockScriptHex(SCRIPT_SIG_1) + assert.isString(addr) + assert.include(addr, 'bitcoincash:') + assert.equal( + addr, + 'bitcoincash:qzd7s66p0mh97s2tqkrwacx4dazwataul5m9f9yw68' + ) + }) + + it('should derive cash address from second sample scriptSig', () => { + const addr = getAddressFromUnlockScriptHex(SCRIPT_SIG_2) + assert.equal( + addr, + 'bitcoincash:qrnr8q2ndfmzgfehz5txel9jpq3lrwhvt5f270fnd4' + ) + }) + }) + + describe('#getSignerAddress', () => { + const SCRIPT_SIG_1 = + '483045022100e15e18048b78e744fe8e440eb2687b5d4eb7009140d5e71950d4351c6e3bf2ef022031b0dab7326846a58e16f6a753e0d011514f827d2adacde63912daa3f85636c5412103833927b98fab40d5f857214d802ba42ea21087105b81e9bff57bf2486c9ff171' + + it('should return signer from txDetails with vin scriptSig', () => { + const txDetails = { + vin: [{ scriptSig: { hex: SCRIPT_SIG_1 } }] + } + const addr = getSignerAddress(txDetails) + assert.equal( + addr, + 'bitcoincash:qzd7s66p0mh97s2tqkrwacx4dazwataul5m9f9yw68' + ) + }) + + it('should return null when no vin', () => { + assert.equal(getSignerAddress({}), null) + }) + }) })