From 7956d597aaf4f06bb38031e7433872bb654a10f8 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 27 Aug 2026 10:02:43 -0700 Subject: [PATCH 1/4] Mark Set Avatar URL complete and set next feature to follow/unfollow By specifier. --- specifier-prompt.md | 25 ++++++++++++++----------- specs/feature-backlog.md | 19 ++++++++++--------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/specifier-prompt.md b/specifier-prompt.md index ca614df..101e3b0 100644 --- a/specifier-prompt.md +++ b/specifier-prompt.md @@ -167,17 +167,17 @@ Goal: reach feature parity with [memo.cash](https://memo.cash). | Tier | Features | Status | |------|----------|--------| | **P0** | Post, Set name, Reply, Efficient pagination | ✅ shipped | -| **P1** | Like counts (read side) ✅; client display ✅; Set profile text ✅; Set profile picture, Follow/Unfollow | next | +| **P1** | Like counts (read side) ✅; client display ✅; Set profile text ✅; Set profile picture ✅; Follow/Unfollow | next | | **P2** | Topics (list, feed, post, follow/unfollow) | later | | **P3** | Polls (create, add option, vote) | later | | **P4** | Mute / unmute user | later | | **P5** | Send money memo action, MIP-0009 token exchange | later | | **P6** | Repost, ranked feed, notifications, search, tags, following feed | later | -**Suggested next spec:** P1.4 — add a "Set Avatar URL" UI on the Account page that -broadcasts the `0x6d0a` Set profile picture action. The indexer and DB already -read/store profile pictures; the missing piece is the client write path and the -Account page editor. +**Suggested next spec:** P1.5/P1.6 — add Follow/Unfollow buttons on the profile +page that broadcast `0x6d06`/`0x6d07`, plus the DB read side (follow state, +following/followers lists). The indexer already stores follows in `followsDb`; +the missing pieces are the client write path and the DB read side. --- @@ -313,6 +313,11 @@ that a single user-facing feature may require specs in more than one component. client Set Bio UI enforces 217. The indexer's `handleSetProfile` still validates against `MAX_POST_SIZE = 65000`; the looser indexer limit is a separate hardening item (protocol parity would use 217). +14. **set-avatar-url Scenario 1 has a tautological assertion (gotcha #12 again).** + The "account page shows my avatar URL as \"\"" assertion echoes the same + example value that was broadcast, so Gherkin mutation of the URL survives + trivially. Same pattern as set-bio Scenario 1. If tightening, tie the + assertion to independent fixture data rather than the broadcast example. --- @@ -350,9 +355,7 @@ At the end of each session, update this file: - Note the current `master` HEAD commit. - State the next feature to work on. -Current `master` HEAD: `e321c7f` (set-bio merged to `master`; client unit 48, -property 6, acceptance all-features, lint, and build passing). An unrelated -`swarmforge.conf` specifier-model switch that leaked in with the merge was -reverted to keep the merge focused on set-bio. -Next action: **spec P1.4** — add a "Set Avatar URL" UI on the Account page that -broadcasts the `0x6d0a` Set profile picture action. +Current `master` HEAD: `ba2b3bf` (set-avatar-url merged to `master`; client unit +74, property 11, acceptance all-features, lint, and build passing). +Next action: **spec P1.5/P1.6** — Follow/Unfollow user (client write path + +DB read side). diff --git a/specs/feature-backlog.md b/specs/feature-backlog.md index 5dfd018..ddb6804 100644 --- a/specs/feature-backlog.md +++ b/specs/feature-backlog.md @@ -122,7 +122,7 @@ adding/editing the client UI. | 1.1 | Like / tip a Memo — read side | `0x6d04` | D | ✅ | `likeCount` returned on `/posts/*` and `/posts/:txid/thread` | | 1.2 | Like / tip a Memo — client display | `0x6d04` | C | ✅ | Feed/Profile/Thread read `likeCount` from API instead of defaulting to 0 | | 1.3 | Set profile text (bio) | `0x6d05` | C | ✅ | C: "Set Bio" UI on Account page broadcasts `0x6d05` (217-byte limit) | -| 1.4 | Set profile picture | `0x6d0a` | C, I, D | 🟡 partial | C: add "Set Avatar URL" UI; I/D already read | +| 1.4 | Set profile picture | `0x6d0a` | C, I, D | ✅ | C: "Set Avatar URL" UI broadcasts `0x6d0a` (217-byte limit); I/D already read | | 1.5 | Follow a user | `0x6d06` | C, I, D | 🔴 missing | C: follow button on profile; D: follow state + following/followers lists | | 1.6 | Unfollow a user | `0x6d07` | C, I, D | 🔴 missing | C: unfollow button; D: follow state | @@ -133,8 +133,8 @@ adding/editing the client UI. thread views now read `likeCount` from the API (profile shows a read-only like button). 3. **Set profile text** ✅ DONE. Account page "Set Bio" UI broadcasts `0x6d05` with a 217-byte limit and byte counter (task `set-bio`). -4. **Set profile picture** — URL broadcast + Account page UI. NEXT. -5. **Follow / Unfollow user** — social graph; enables the following feed later. +4. **Set profile picture** ✅ DONE. Account page "Set Avatar URL" UI broadcasts `0x6d0a` with a 217-byte limit and byte counter (task `set-avatar-url`). +5. **Follow / Unfollow user** — social graph; enables the following feed later. NEXT. ### Like / tip details @@ -213,13 +213,14 @@ Polls require a new data model and rendering. The indexer has no handler yet. ## Suggested next spec -**Set profile picture (bio)** (P1.4): -- `psf-memo-client`: add a "Set Avatar URL" UI on the Account page that broadcasts - the `0x6d0a` Set profile picture action via `minimal-slp-wallet.sendOpReturn()`. -- The indexer and DB already read/store profile pictures; the missing piece is - the client write path and the Account page editor. +**Follow / Unfollow user** (P1.5 / P1.6): +- `psf-memo-client`: add a Follow button on the profile page that broadcasts the + `0x6d06` Follow action (and an Unfollow button broadcasting `0x6d07`). +- `psf-memo-db`: expose follow state and following/followers lists. +- The indexer already stores follows in `followsDb`; the missing pieces are the + client write path and the DB read side. -This is the next smallest end-to-end win after the set-bio write path closed. +This is the next smallest end-to-end win after the set-avatar-url write path closed. --- From d901cfc6cb6f86cd5c7cc20419cff6ebf4bc95c5 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 27 Aug 2026 10:17:43 -0700 Subject: [PATCH 2/4] Spec follow/unfollow user (client write path + DB read side) Add Gherkin specs for the Follow/Unfollow button (0x6d06/0x6d07) and the DB follow state + following/followers read side. Use bch-js Address tools for cashaddr<->hash160 conversion instead of a new dependency. By specifier. --- psf-memo-client/specs/follow-user.feature | 55 +++++++++++++++++++++++ psf-memo-db/specs/follow-read.feature | 39 ++++++++++++++++ specifier-prompt.md | 6 +++ specs/feature-backlog.md | 15 +++++++ 4 files changed, 115 insertions(+) create mode 100644 psf-memo-client/specs/follow-user.feature create mode 100644 psf-memo-db/specs/follow-read.feature diff --git a/psf-memo-client/specs/follow-user.feature b/psf-memo-client/specs/follow-user.feature new file mode 100644 index 0000000..8b9d7db --- /dev/null +++ b/psf-memo-client/specs/follow-user.feature @@ -0,0 +1,55 @@ +# Scenarios: Follow User - 1, Follow User - 2, Follow User - 3, Follow User - 4, Follow User - 5 +# +# The follow/unfollow OP_RETURN payload is the followee's 20-byte hash160. +# Convert the followee's cash address with bch-js Address.toHash160() (available +# through the minimal-slp-wallet embedded bch-js) before broadcasting 0x6d06 / +# 0x6d07. Do not add a separate cashaddr dependency. +Feature: Follow User + + Background: + Given a wallet authenticated for the address bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d + Given the wallet has spendable output to pay the transaction fee + + Scenario Outline: Follow User - 1 viewing another user's profile shows a Follow button + Given I open the profile page for the address + Then the profile page shows a Follow button + + Examples: + | addr | + | bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy | + | bitcoincash:qqq3728yw0y47sqn6l2na30mcw6zm78dzqre909m2r | + + Scenario Outline: Follow User - 2 clicking Follow broadcasts the follow action and shows Unfollow + Given I open the profile page for the address + When I click the Follow button + Then the app broadcasts an OP_RETURN transaction with the Memo follow prefix for the address + Then the profile page shows an Unfollow button + + Examples: + | addr | + | bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy | + + Scenario Outline: Follow User - 3 clicking Unfollow broadcasts the unfollow action and shows Follow + Given the psf-memo-db API reports that I follow the address + Given I open the profile page for the address + When I click the Unfollow button + Then the app broadcasts an OP_RETURN transaction with the Memo unfollow prefix for the address + Then the profile page shows a Follow button + + Examples: + | addr | + | bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy | + + Scenario: Follow User - 4 viewing my own profile does not show a Follow button + Given I open the profile page for my own address + Then the profile page does not show a Follow button + + Scenario Outline: Follow User - 5 the profile page shows Unfollow when I already follow the user + Given the psf-memo-db API reports that I follow the address + Given I open the profile page for the address + Then the profile page shows an Unfollow button + + Examples: + | addr | + | bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy | + | bitcoincash:qqq3728yw0y47sqn6l2na30mcw6zm78dzqre909m2r | diff --git a/psf-memo-db/specs/follow-read.feature b/psf-memo-db/specs/follow-read.feature new file mode 100644 index 0000000..d50acc2 --- /dev/null +++ b/psf-memo-db/specs/follow-read.feature @@ -0,0 +1,39 @@ +# Scenarios: Follow Read - 1, Follow Read - 2, Follow Read - 3 +# +# The follows store keys followees by 20-byte hash160. Use bch-js +# Address.toHash160() / Address.hash160ToCash() to convert between cash +# addresses and hash160 for the follow state and following/followers lists. +# Prefer bch-js over adding a separate cashaddr dependency. +Feature: Follow Read + + Background: + Given a psf-memo-db instance with a follows store + Given the fixture "follows" is loaded into the follows store + + Scenario Outline: Follow Read - 1 GET /follow/state reports whether a follower follows a followee + When the client requests the follow state for follower and followee + Then the follow state reports following + + Examples: + | follower | followee | following | + | bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d | bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy | true | + | bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d | bitcoincash:qqq3728yw0y47sqn6l2na30mcw6zm78dzqre909m2r | false | + | bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a | bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy | true | + + Scenario Outline: Follow Read - 2 GET /follow/following lists the addresses a follower follows + When the client requests the following list for + Then the following list contains the addresses + + Examples: + | follower | expected | + | bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d | bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy | + | bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a | bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy | + + Scenario Outline: Follow Read - 3 GET /follow/followers lists the addresses that follow a followee + When the client requests the followers list for + Then the followers list contains the addresses + + Examples: + | followee | expected | + | bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy | bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d,bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a | + | bitcoincash:qqq3728yw0y47sqn6l2na30mcw6zm78dzqre909m2r | | diff --git a/specifier-prompt.md b/specifier-prompt.md index 101e3b0..f9368d5 100644 --- a/specifier-prompt.md +++ b/specifier-prompt.md @@ -318,6 +318,12 @@ that a single user-facing feature may require specs in more than one component. example value that was broadcast, so Gherkin mutation of the URL survives trivially. Same pattern as set-bio Scenario 1. If tightening, tie the assertion to independent fixture data rather than the broadcast example. +15. **Use bch-js for cashaddr conversion, not a new dependency.** The follow + (`0x6d06`) / unfollow (`0x6d07`) payload is the followee's 20-byte hash160 + (P2PKH). Convert with `bchjs.Address.toHash160()` (client, via the + minimal-slp-wallet embedded bch-js) and `bchjs.Address.hash160ToCash()` + (DB read side). Prefer bch-js over installing a separate cashaddr library. + See `specs/feature-backlog.md` "Suggested next spec" for the follow feature. --- diff --git a/specs/feature-backlog.md b/specs/feature-backlog.md index ddb6804..aa71ace 100644 --- a/specs/feature-backlog.md +++ b/specs/feature-backlog.md @@ -220,6 +220,21 @@ Polls require a new data model and rendering. The indexer has no handler yet. - The indexer already stores follows in `followsDb`; the missing pieces are the client write path and the DB read side. +### cashaddr conversion (use bch-js, not a new dependency) + +The follow/unfollow OP_RETURN payload is the followee's **20-byte hash160** +(P2PKH). The `followsDb` store keys followees by that hash160. Convert between +cash addresses and hash160 with **bch-js** `Address` tools, which are already +available and preferred over adding a separate cashaddr library: + +- Client: `bchjs.Address.toHash160(followeeCashAddress)` returns the hash160 + hex for the `0x6d06` / `0x6d07` payload. bch-js is embedded in + `minimal-slp-wallet`, so no new client dependency is needed. +- DB read side: `bchjs.Address.toHash160(followeeCashAddress)` for the follow + state lookup, and `bchjs.Address.hash160ToCash(followeePkHash)` to return + cash addresses in the following/followers lists. Add `@psf/bch-js` to + `psf-memo-db` rather than a separate cashaddr package. + This is the next smallest end-to-end win after the set-avatar-url write path closed. --- From 247d4a9fcf0ba9d78c6a7d8ae412565314ebfcfd Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 27 Aug 2026 10:35:33 -0700 Subject: [PATCH 3/4] Implement follow-user and follow-read behavior - Add MemoFollow service for follow/unfollow broadcast with hash160 payloads - Update Profiles store with follow state - Extend ProfilePage and Profile UI with Follow/Unfollow buttons - Add psf-memo-db /follow/state, /follow/following, /follow/followers endpoints - Add FollowQuery adapter, use cases, REST controller, and unit tests - Update acceptance handlers for both components By coder. --- psf-memo-client/acceptance/lib/handlers.js | 168 +- .../src/components/app-body/profile/index.js | 70 +- psf-memo-client/src/services/memo-db.js | 18 + psf-memo-client/src/services/memo-follow.js | 116 + psf-memo-client/src/services/profile-page.js | 59 +- psf-memo-client/src/services/profiles.js | 15 + psf-memo-client/test/unit/memo-follow.test.js | 141 + .../test/unit/profile-page.test.js | 102 +- psf-memo-client/test/unit/profiles.test.js | 40 + psf-memo-db/acceptance/lib/handlers.js | 114 + psf-memo-db/package-lock.json | 2267 +++++++++++++++-- psf-memo-db/package.json | 1 + psf-memo-db/src/adapters/follow-query.js | 84 + psf-memo-db/src/adapters/index.js | 4 + .../controllers/rest-api/follow/controller.js | 111 + .../src/controllers/rest-api/follow/index.js | 36 + psf-memo-db/src/controllers/rest-api/index.js | 4 + psf-memo-db/src/use-cases/follow-state.js | 34 + psf-memo-db/src/use-cases/index.js | 18 + psf-memo-db/src/use-cases/list-followers.js | 31 + psf-memo-db/src/use-cases/list-following.js | 31 + .../test/unit/adapters/follow-query.unit.js | 112 + .../controllers/follow.controller.unit.js | 75 + .../test/unit/use-cases/follow-state.unit.js | 72 + .../unit/use-cases/list-followers.unit.js | 45 + .../unit/use-cases/list-following.unit.js | 45 + 26 files changed, 3523 insertions(+), 290 deletions(-) create mode 100644 psf-memo-client/src/services/memo-follow.js create mode 100644 psf-memo-client/test/unit/memo-follow.test.js create mode 100644 psf-memo-db/src/adapters/follow-query.js create mode 100644 psf-memo-db/src/controllers/rest-api/follow/controller.js create mode 100644 psf-memo-db/src/controllers/rest-api/follow/index.js create mode 100644 psf-memo-db/src/use-cases/follow-state.js create mode 100644 psf-memo-db/src/use-cases/list-followers.js create mode 100644 psf-memo-db/src/use-cases/list-following.js create mode 100644 psf-memo-db/test/unit/adapters/follow-query.unit.js create mode 100644 psf-memo-db/test/unit/controllers/follow.controller.unit.js create mode 100644 psf-memo-db/test/unit/use-cases/follow-state.unit.js create mode 100644 psf-memo-db/test/unit/use-cases/list-followers.unit.js create mode 100644 psf-memo-db/test/unit/use-cases/list-following.unit.js diff --git a/psf-memo-client/acceptance/lib/handlers.js b/psf-memo-client/acceptance/lib/handlers.js index 4910faf..6b0a676 100644 --- a/psf-memo-client/acceptance/lib/handlers.js +++ b/psf-memo-client/acceptance/lib/handlers.js @@ -32,6 +32,7 @@ const SetAvatarUrlPage = require('../../src/services/set-avatar-url-page') const AccountPage = require('../../src/services/account-page') const MemoLike = require('../../src/services/memo-like') const LikeTipPage = require('../../src/services/like-tip-page') +const MemoFollow = require('../../src/services/memo-follow') const RecentFeedPage = require('../../src/services/recent-feed-page') const ProfilePage = require('../../src/services/profile-page') const ThreadPage = require('../../src/services/thread-page') @@ -42,6 +43,8 @@ const MEMO_SET_NAME_PREFIX = MemoSetName.MEMO_SET_NAME_PREFIX const MEMO_SET_BIO_PREFIX = MemoSetBio.MEMO_SET_BIO_PREFIX const MEMO_SET_AVATAR_URL_PREFIX = MemoSetAvatarUrl.MEMO_SET_AVATAR_URL_PREFIX const MEMO_LIKE_PREFIX = MemoLike.MEMO_LIKE_PREFIX +const MEMO_FOLLOW_PREFIX = MemoFollow.MEMO_FOLLOW_PREFIX +const MEMO_UNFOLLOW_PREFIX = MemoFollow.MEMO_UNFOLLOW_PREFIX // Default author address used by Gherkin steps that refer to "the author address". const AUTHOR_ADDRESS = 'bitcoincash:qz7v6ztvzu2f2xd2ww8pnx9vwk0g4ncvfvavktg0jc' @@ -55,6 +58,14 @@ const THIRD_ADDRESS = 'bitcoincash:third-address' function makeWallet (address) { const wallet = { walletInfo: { cashAddress: address }, + bchjs: { + Address: { + toHash160 (addr) { + // Stable fake hash160: first 20 bytes of sha256 of the address. + return require('crypto').createHash('sha256').update(addr).digest('hex').slice(0, 40) + } + } + }, utxos: [], broadcasts: [], getUtxos: async function () { @@ -79,21 +90,28 @@ function makeFeed () { } } -// A fake profile store recording display names, bios, and avatar URLs set for addresses. +// A fake profile store recording display names, bios, avatar URLs, and follow state. function makeProfiles () { const names = {} const bios = {} const avatarUrls = {} + const following = {} return { names, bios, avatarUrls, + following, setName: (addr, name) => { names[addr] = name }, getName: (addr) => names[addr] || null, setBio: (addr, bio) => { bios[addr] = bio }, getBio: (addr) => bios[addr] || null, setAvatarUrl: (addr, url) => { avatarUrls[addr] = url }, - getAvatarUrl: (addr) => avatarUrls[addr] || null + getAvatarUrl: (addr) => avatarUrls[addr] || null, + setFollowState: (selfAddr, targetAddr, isFollowing) => { + if (!following[selfAddr]) following[selfAddr] = {} + following[selfAddr][targetAddr] = isFollowing + }, + getFollowState: (selfAddr, targetAddr) => following[selfAddr]?.[targetAddr] || false } } @@ -112,16 +130,21 @@ function makeThread () { function makeMemoDb () { const posts = [] const threads = {} + const followState = {} return { posts, threads, + followState, addPost (post) { posts.push(post) }, addThread (txid, thread) { threads[txid] = thread }, + setFollowState (followerAddr, followeeAddr, following) { + followState[`${followerAddr}:${followeeAddr}`] = following + }, async getRecentPosts ({ limit = 100, offset = 0 } = {}) { const page = posts.slice(offset, offset + limit) return { posts: page, pagination: { total: posts.length, limit, offset, hasMore: offset + page.length < posts.length } } @@ -133,6 +156,9 @@ function makeMemoDb () { }, async getPostThread (txid) { return threads[txid] || { post: null } + }, + async getFollowState (followerAddr, followeeAddr) { + return followState[`${followerAddr}:${followeeAddr}`] || false } } } @@ -141,10 +167,12 @@ function makeMemoDb () { function createWorld () { const wallet = makeWallet('') const feed = makeFeed() + const profiles = makeProfiles() const memoPost = new MemoPost({ wallet, feed }) const thread = makeThread() const memoReply = new MemoReply({ wallet, thread }) const memoLike = new MemoLike({ wallet, feed }) + const memoFollow = new MemoFollow({ wallet, profiles }) const memoDb = makeMemoDb() const world = { @@ -154,6 +182,7 @@ function createWorld () { memoPost, memoReply, memoLike, + memoFollow, memoDb, currentPath: null, menuOpen: false, @@ -185,7 +214,6 @@ function createWorld () { // The Set Name Page and Account Page controllers share a profile store so // a name set on one page is visible on the other. - const profiles = makeProfiles() const memoSetName = new MemoSetName({ wallet, profiles }) world.setNamePage = new SetNamePage({ memoSetName, @@ -1182,6 +1210,140 @@ const handlers = [ throw new Error(`Expected like count ${expected} for reply ${txid}, got ${actual}.`) } } + }, + { + name: 'API reports I follow address', + pattern: /^the psf-memo-db API reports that I follow the address (.+)$/, + run (m, example, world) { + const addr = resolveParam(m[1], example) + const myAddr = world.wallet.walletInfo.cashAddress + world.memoDb.setFollowState(myAddr, addr, true) + } + }, + { + name: 'open profile page for address', + pattern: /^I open the profile page for the address (.+)$/, + async run (m, example, world) { + const addr = resolveParam(m[1], example) + const myAddr = world.wallet.walletInfo.cashAddress + world.profilePage = new ProfilePage({ + memoDb: world.memoDb, + addr, + myAddr, + memoFollow: world.memoFollow + }) + await world.profilePage.load() + world.currentPath = `${ProfilePage.PROFILE_PATH_PREFIX}/${encodeURIComponent(addr)}` + } + }, + { + name: 'open profile page for own address', + pattern: /^I open the profile page for my own address$/, + async run (m, example, world) { + const myAddr = world.wallet.walletInfo.cashAddress + world.profilePage = new ProfilePage({ + memoDb: world.memoDb, + addr: myAddr, + myAddr, + memoFollow: world.memoFollow + }) + await world.profilePage.load() + world.currentPath = `${ProfilePage.PROFILE_PATH_PREFIX}/${encodeURIComponent(myAddr)}` + } + }, + { + name: 'profile page shows Follow button', + pattern: /^the profile page shows a Follow button$/, + run (m, example, world) { + if (!world.profilePage) { + throw new Error('No profile page is loaded.') + } + if (!world.profilePage.canFollow()) { + throw new Error('Profile page cannot show a Follow button for this address.') + } + if (world.profilePage.isFollowing()) { + throw new Error('Profile page shows Unfollow, but Follow was expected.') + } + } + }, + { + name: 'profile page shows Unfollow button', + pattern: /^the profile page shows an Unfollow button$/, + run (m, example, world) { + if (!world.profilePage) { + throw new Error('No profile page is loaded.') + } + if (!world.profilePage.canFollow()) { + throw new Error('Profile page cannot show an Unfollow button for this address.') + } + if (!world.profilePage.isFollowing()) { + throw new Error('Profile page shows Follow, but Unfollow was expected.') + } + } + }, + { + name: 'profile page does not show Follow button', + pattern: /^the profile page does not show a Follow button$/, + run (m, example, world) { + if (!world.profilePage) { + throw new Error('No profile page is loaded.') + } + if (world.profilePage.canFollow()) { + throw new Error('Profile page should not show a Follow button.') + } + } + }, + { + name: 'click Follow button', + pattern: /^I click the Follow button$/, + async run (m, example, world) { + await world.profilePage.follow() + } + }, + { + name: 'click Unfollow button', + pattern: /^I click the Unfollow button$/, + async run (m, example, world) { + await world.profilePage.unfollow() + } + }, + { + name: 'broadcasts OP_RETURN with Memo follow prefix for address', + pattern: /^the app broadcasts an OP_RETURN transaction with the Memo follow prefix for the address (.+)$/, + run (m, example, world) { + const addr = resolveParam(m[1], example) + const hash160 = world.wallet.bchjs.Address.toHash160(addr) + const broadcasts = world.wallet.broadcasts + if (!broadcasts.length) { + throw new Error('No OP_RETURN transaction was broadcast.') + } + const last = broadcasts[broadcasts.length - 1] + if (last.prefix !== MEMO_FOLLOW_PREFIX) { + throw new Error(`Expected Memo follow prefix ${MEMO_FOLLOW_PREFIX}, got "${last.prefix}".`) + } + if (last.msg.toString('hex') !== hash160) { + throw new Error(`Broadcast follow hash160 did not match ${addr}.`) + } + } + }, + { + name: 'broadcasts OP_RETURN with Memo unfollow prefix for address', + pattern: /^the app broadcasts an OP_RETURN transaction with the Memo unfollow prefix for the address (.+)$/, + run (m, example, world) { + const addr = resolveParam(m[1], example) + const hash160 = world.wallet.bchjs.Address.toHash160(addr) + const broadcasts = world.wallet.broadcasts + if (!broadcasts.length) { + throw new Error('No OP_RETURN transaction was broadcast.') + } + const last = broadcasts[broadcasts.length - 1] + if (last.prefix !== MEMO_UNFOLLOW_PREFIX) { + throw new Error(`Expected Memo unfollow prefix ${MEMO_UNFOLLOW_PREFIX}, got "${last.prefix}".`) + } + if (last.msg.toString('hex') !== hash160) { + throw new Error(`Broadcast unfollow hash160 did not match ${addr}.`) + } + } } ] diff --git a/psf-memo-client/src/components/app-body/profile/index.js b/psf-memo-client/src/components/app-body/profile/index.js index f7a0b1f..49a0595 100644 --- a/psf-memo-client/src/components/app-body/profile/index.js +++ b/psf-memo-client/src/components/app-body/profile/index.js @@ -4,10 +4,12 @@ import React, { useState, useEffect } from 'react' import { useParams } from 'react-router-dom' -import { Container, Row, Col, Spinner, Card } from 'react-bootstrap' +import { Container, Row, Col, Spinner, Card, Button } from 'react-bootstrap' import Jdenticon from '@chris.troutner/react-jdenticon' import MemoDb from '../../../services/memo-db' +import MemoFollow from '../../../services/memo-follow' +import ProfilePage from '../../../services/profile-page' import PostReplyCount from '../../post-reply-count' import LikeButton from '../../post-feed/like-button' import PostThreadModal from '../../post-thread-modal' @@ -59,6 +61,8 @@ function Profile (props) { const [threadTxid, setThreadTxid] = useState(null) const [showThreadModal, setShowThreadModal] = useState(false) const [profiles, setProfiles] = useState({}) + const [profilePage, setProfilePage] = useState(null) + const [busy, setBusy] = useState(false) const openThread = (txid) => { setThreadTxid(txid) @@ -70,6 +74,28 @@ function Profile (props) { setThreadTxid(null) } + const handleFollow = async () => { + if (!profilePage || busy) return + setBusy(true) + try { + await profilePage.follow() + } catch (err) { + setError(err.message || 'Failed to follow') + } + setBusy(false) + } + + const handleUnfollow = async () => { + if (!profilePage || busy) return + setBusy(true) + try { + await profilePage.unfollow() + } catch (err) { + setError(err.message || 'Failed to unfollow') + } + setBusy(false) + } + useEffect(() => { const loadProfile = async () => { setLoading(true) @@ -77,16 +103,23 @@ function Profile (props) { try { const memoDb = new MemoDb() - const [profile, profilePic, postsData] = await Promise.all([ + const myAddr = appData?.wallet?.walletInfo?.cashAddress || null + const memoFollow = myAddr + ? new MemoFollow({ wallet: appData.wallet, profiles: appData.profiles }) + : null + const page = new ProfilePage({ memoDb, addr, myAddr, memoFollow }) + + const [profile, profilePic, pageData] = await Promise.all([ memoDb.getProfile(addr), memoDb.getProfilePic(addr), - memoDb.getPostsByAddr(addr, { limit: 100, offset: 0 }) + page.load() ]) setProfileText(profile?.text || '') setProfilePicUrl(profilePic?.url || null) - setPosts(postsData.posts || []) - setPagination(postsData.pagination || null) + setPosts(pageData.posts || []) + setPagination(pageData.pagination || null) + setProfilePage(page) setProfiles({}) // Future: load profile names for the post list. } catch (err) { setError(err.message || 'Failed to load profile') @@ -101,7 +134,10 @@ function Profile (props) { setError('Missing profile address') setLoading(false) } - }, [addr]) + }, [addr, appData?.wallet, appData?.profiles]) + + const showFollowButton = profilePage && profilePage.canFollow() && !profilePage.isFollowing() + const showUnfollowButton = profilePage && profilePage.canFollow() && profilePage.isFollowing() return ( @@ -131,6 +167,28 @@ function Profile (props) { BCH {addr} + {showFollowButton && ( + + )} + {showUnfollowButton && ( + + )} diff --git a/psf-memo-client/src/services/memo-db.js b/psf-memo-client/src/services/memo-db.js index 2eb49ab..32f5141 100644 --- a/psf-memo-client/src/services/memo-db.js +++ b/psf-memo-client/src/services/memo-db.js @@ -30,6 +30,24 @@ class MemoDb { return this.getLevelResource('name', addr, 'getName') } + async getFollowState (followerAddr, followeeAddr) { + try { + const result = await this.axios.get( + `${config.backend}/follow/state`, + { + params: { + follower: followerAddr, + followee: followeeAddr + } + } + ) + return result.data.following === true + } catch (err) { + console.error('Error in getFollowState()') + throw err + } + } + // GET a paginated 'recent' listing endpoint. async getRecent (path, name, params) { try { diff --git a/psf-memo-client/src/services/memo-follow.js b/psf-memo-client/src/services/memo-follow.js new file mode 100644 index 0000000..0b533a8 --- /dev/null +++ b/psf-memo-client/src/services/memo-follow.js @@ -0,0 +1,116 @@ +/* + Memo follow/unfollow behavior: compose, validate, and broadcast a Memo + follow (0x6d06) or unfollow (0x6d07) action. + + A follow transaction carries the followee's 20-byte hash160. The cash + address is converted with the wallet's embedded bch-js Address.toHash160() + so no separate cashaddr dependency is needed. + + The wallet and an injected profile store are used so this module stays + testable and free of UI/network concerns; environmentally unsuitable I/O + lives behind those small adapter boundaries. + + Constants + MEMO_FOLLOW_PREFIX : hex prefix for the Memo "follow" action (0x6d06) + MEMO_UNFOLLOW_PREFIX : hex prefix for the Memo "unfollow" action (0x6d07) + PK_HASH_LENGTH : size of the followee hash160 in bytes (20) +*/ + +const MemoAction = require('./memo-action') + +const MEMO_FOLLOW_PREFIX = '6d06' +const MEMO_UNFOLLOW_PREFIX = '6d07' +const PK_HASH_LENGTH = 20 + +class MemoFollow extends MemoAction { + static config = { + prefix: MEMO_FOLLOW_PREFIX, + walletRequiredMsg: 'Memo follow requires a wallet.', + lengthMessage: 'Follow address is invalid.', + emptyMessage: 'Follow address is required.', + lengthCode: 'follow_validation', + validationCode: 'follow_validation' + } + + constructor (deps = {}) { + super(deps) + this.profiles = deps.profiles + } + + // Validate a candidate cash address. Returns { ok: true } or throws a typed + // validation error. + validate (addr) { + if (typeof addr !== 'string' || addr.trim().length === 0) { + const err = new Error(this.emptyMessage) + err.code = this.validationCode + throw err + } + + try { + this._toHash160(addr) + return { ok: true } + } catch (err) { + const validationErr = new Error(`Invalid cash address: ${addr}`) + validationErr.code = this.validationCode + throw validationErr + } + } + + // Broadcast a Memo follow for the given followee address. + async follow (followeeAddr) { + return this._broadcastAction(followeeAddr, MEMO_FOLLOW_PREFIX, true) + } + + // Broadcast a Memo unfollow for the given followee address. + async unfollow (followeeAddr) { + return this._broadcastAction(followeeAddr, MEMO_UNFOLLOW_PREFIX, false) + } + + // Internal: validate, broadcast, and reflect a follow/unfollow action. + async _broadcastAction (followeeAddr, prefix, isFollow) { + if (!this.wallet) { + throw new Error(this.walletRequiredMsg) + } + this._ensureBchjs() + + this.validate(followeeAddr) + + await this.wallet.getUtxos() + + const hash160 = this._toHash160(followeeAddr) + const raw = Buffer.from(hash160, 'hex') + + const txid = await this.wallet.sendOpReturn(raw, prefix) + + this.reflect(txid, followeeAddr, isFollow) + + return txid + } + + // Convert a cash address to its 20-byte hash160 hex string using the wallet's + // embedded bch-js. + _toHash160 (addr) { + return this.wallet.bchjs.Address.toHash160(addr) + } + + _ensureBchjs () { + if (!this.wallet.bchjs || typeof this.wallet.bchjs.Address.toHash160 !== 'function') { + throw new Error('Wallet does not expose bch-js Address.toHash160.') + } + } + + // Record the new follow state on the injected profile store when it exposes + // the follow methods. + reflect (txid, followeeAddr, isFollow) { + if (this.profiles && typeof this.profiles.setFollowState === 'function') { + const myAddr = this.wallet?.walletInfo?.cashAddress + this.profiles.setFollowState(myAddr, followeeAddr, isFollow) + } + } +} + +MemoFollow.MEMO_FOLLOW_PREFIX = MEMO_FOLLOW_PREFIX +MemoFollow.MEMO_UNFOLLOW_PREFIX = MEMO_UNFOLLOW_PREFIX +MemoFollow.PK_HASH_LENGTH = PK_HASH_LENGTH + +module.exports = MemoFollow diff --git a/psf-memo-client/src/services/profile-page.js b/psf-memo-client/src/services/profile-page.js index 25efbaf..292c3f3 100644 --- a/psf-memo-client/src/services/profile-page.js +++ b/psf-memo-client/src/services/profile-page.js @@ -1,13 +1,14 @@ /* - Profile Page behavior: load and display a single address's Memo posts. + Profile Page behavior: load and display a single address's Memo posts, + follow state, and follow/unfollow controls. This is the testable controller behind the React "Profile" page. It wraps - the MemoDb client, targets a specific address, and exposes the loaded posts so - the view can render per-post data such as the like count. + the MemoDb client, targets a specific address, exposes the loaded posts, + and coordinates follow state with an injected MemoFollow action. - The memoDb and address concerns are injected so this module stays free of - UI/network concerns; environmentally unsuitable I/O lives behind those small - adapter boundaries. + The memoDb, address, viewer address, and memoFollow concerns are injected so + this module stays free of UI/network concerns; environmentally unsuitable + I/O lives behind those small adapter boundaries. */ const PROFILE_PATH_PREFIX = '/profile' @@ -16,8 +17,11 @@ class ProfilePage { constructor (deps = {}) { this.memoDb = deps.memoDb || null this.addr = deps.addr || null + this.myAddr = deps.myAddr || null + this.memoFollow = deps.memoFollow || null this.posts = [] this.pagination = null + this.followState = null } async load ({ limit = 100, offset = 0 } = {}) { @@ -32,7 +36,48 @@ class ProfilePage { this.posts = data.posts || [] this.pagination = data.pagination || null - return { posts: this.posts, pagination: this.pagination } + if (this.myAddr && !this.isOwnProfile()) { + this.followState = await this.memoDb.getFollowState(this.myAddr, this.addr) + } else { + this.followState = false + } + + return { + posts: this.posts, + pagination: this.pagination, + followState: this.followState, + isOwnProfile: this.isOwnProfile() + } + } + + isOwnProfile () { + return Boolean(this.myAddr) && this.myAddr === this.addr + } + + canFollow () { + return Boolean(this.myAddr) && !this.isOwnProfile() + } + + isFollowing () { + return this.followState === true + } + + async follow () { + if (!this.memoFollow) { + throw new Error('Profile page requires a memo follow handler.') + } + await this.memoFollow.follow(this.addr) + this.followState = true + return { ok: true } + } + + async unfollow () { + if (!this.memoFollow) { + throw new Error('Profile page requires a memo follow handler.') + } + await this.memoFollow.unfollow(this.addr) + this.followState = false + return { ok: true } } getPost (txid) { diff --git a/psf-memo-client/src/services/profiles.js b/psf-memo-client/src/services/profiles.js index 58f7084..63fcd26 100644 --- a/psf-memo-client/src/services/profiles.js +++ b/psf-memo-client/src/services/profiles.js @@ -14,6 +14,7 @@ class Profiles { this.names = new Map() this.bios = new Map() this.avatarUrls = new Map() + this.following = new Map() } setName (addr, name) { @@ -45,6 +46,20 @@ class Profiles { if (!addr) return null return this.avatarUrls.get(addr) || null } + + // Track whether the current wallet follows a given address. + setFollowState (selfAddr, targetAddr, isFollowing) { + if (!selfAddr || !targetAddr) return + if (!this.following.has(selfAddr)) { + this.following.set(selfAddr, new Map()) + } + this.following.get(selfAddr).set(targetAddr, isFollowing) + } + + getFollowState (selfAddr, targetAddr) { + if (!selfAddr || !targetAddr) return false + return this.following.get(selfAddr)?.get(targetAddr) || false + } } module.exports = Profiles diff --git a/psf-memo-client/test/unit/memo-follow.test.js b/psf-memo-client/test/unit/memo-follow.test.js new file mode 100644 index 0000000..216b07c --- /dev/null +++ b/psf-memo-client/test/unit/memo-follow.test.js @@ -0,0 +1,141 @@ +/* + Unit tests for the Memo follow/unfollow behavior. + + The follow action validates the followee cash address, converts it to a + 20-byte hash160, and broadcasts it with the Memo follow (0x6d06) or unfollow + (0x6d07) prefix. A successful broadcast is reflected on the injected profile + store. +*/ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert/strict') +const MemoFollow = require('../../src/services/memo-follow') + +const MY_ADDRESS = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' +const FOLLOWEE_ADDRESS = 'bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy' +const FOLLOWEE_HASH160 = 'cb481232299cd5743151ac4b2d63ae198e7bb0a9' + +function makeBchjs () { + return { + Address: { + toHash160 (addr) { + if (addr === FOLLOWEE_ADDRESS) return FOLLOWEE_HASH160 + throw new Error('unsupported address in test') + } + } + } +} + +function makeWallet (address = MY_ADDRESS) { + return { + walletInfo: { cashAddress: address }, + bchjs: makeBchjs(), + broadcasts: [], + async getUtxos () { + return [] + }, + async sendOpReturn (msg, prefix) { + this.broadcasts.push({ msg, prefix }) + return 'aa'.repeat(32) + } + } +} + +function makeProfiles () { + const state = {} + return { + state, + setFollowState: (selfAddr, targetAddr, isFollowing) => { + if (!state[selfAddr]) state[selfAddr] = {} + state[selfAddr][targetAddr] = isFollowing + }, + getFollowState: (selfAddr, targetAddr) => state[selfAddr]?.[targetAddr] || false + } +} + +test('follow broadcasts with the Memo follow prefix and hash160 payload', async () => { + const wallet = makeWallet() + const memoFollow = new MemoFollow({ wallet }) + + await memoFollow.follow(FOLLOWEE_ADDRESS) + + assert.equal(wallet.broadcasts.length, 1) + assert.equal(wallet.broadcasts[0].prefix, MemoFollow.MEMO_FOLLOW_PREFIX) + assert.ok(Buffer.isBuffer(wallet.broadcasts[0].msg)) + assert.equal(wallet.broadcasts[0].msg.toString('hex'), FOLLOWEE_HASH160) +}) + +test('unfollow broadcasts with the Memo unfollow prefix and hash160 payload', async () => { + const wallet = makeWallet() + const memoFollow = new MemoFollow({ wallet }) + + await memoFollow.unfollow(FOLLOWEE_ADDRESS) + + assert.equal(wallet.broadcasts.length, 1) + assert.equal(wallet.broadcasts[0].prefix, MemoFollow.MEMO_UNFOLLOW_PREFIX) + assert.equal(wallet.broadcasts[0].msg.toString('hex'), FOLLOWEE_HASH160) +}) + +test('follow reflects the new follow state on the profile store', async () => { + const wallet = makeWallet() + const profiles = makeProfiles() + const memoFollow = new MemoFollow({ wallet, profiles }) + + await memoFollow.follow(FOLLOWEE_ADDRESS) + + assert.equal(profiles.getFollowState(MY_ADDRESS, FOLLOWEE_ADDRESS), true) +}) + +test('unfollow reflects the new unfollow state on the profile store', async () => { + const wallet = makeWallet() + const profiles = makeProfiles() + const memoFollow = new MemoFollow({ wallet, profiles }) + + await memoFollow.unfollow(FOLLOWEE_ADDRESS) + + assert.equal(profiles.getFollowState(MY_ADDRESS, FOLLOWEE_ADDRESS), false) +}) + +test('follow rejects an empty address', async () => { + const wallet = makeWallet() + const memoFollow = new MemoFollow({ wallet }) + + await assert.rejects( + () => memoFollow.follow(''), + { code: 'follow_validation', message: /Follow address is required/ } + ) + assert.equal(wallet.broadcasts.length, 0) +}) + +test('follow requires a wallet', async () => { + const memoFollow = new MemoFollow({}) + + await assert.rejects( + () => memoFollow.follow(FOLLOWEE_ADDRESS), + /Memo follow requires a wallet/ + ) +}) + +test('follow requires bch-js on the wallet', async () => { + const wallet = makeWallet() + wallet.bchjs = null + const memoFollow = new MemoFollow({ wallet }) + + await assert.rejects( + () => memoFollow.follow(FOLLOWEE_ADDRESS), + /Wallet does not expose bch-js Address.toHash160/ + ) +}) + +test('follow surfaces a broadcast failure', async () => { + const wallet = makeWallet() + wallet.sendOpReturn = async () => { throw new Error('broadcast failed') } + const memoFollow = new MemoFollow({ wallet }) + + await assert.rejects( + () => memoFollow.follow(FOLLOWEE_ADDRESS), + /broadcast failed/ + ) +}) diff --git a/psf-memo-client/test/unit/profile-page.test.js b/psf-memo-client/test/unit/profile-page.test.js index 5211877..2aff2af 100644 --- a/psf-memo-client/test/unit/profile-page.test.js +++ b/psf-memo-client/test/unit/profile-page.test.js @@ -1,8 +1,9 @@ /* Unit tests for the profile page controller. - The profile page loads a single address's posts from the MemoDb client. The - like count returned by the API must be preserved so the view can display it. + The profile page loads a single address's posts from the MemoDb client and + fetches the follow state for the viewer. The like count returned by the API + must be preserved so the view can display it. */ 'use strict' @@ -11,10 +12,24 @@ const test = require('node:test') const assert = require('node:assert/strict') const ProfilePage = require('../../src/services/profile-page') -function makeMemoDb (postsByAddr) { +function makeMemoDb (postsByAddr, followState = {}) { return { async getPostsByAddr (addr, { limit, offset }) { return { posts: postsByAddr[addr] || [], pagination: { total: (postsByAddr[addr] || []).length } } + }, + async getFollowState (followerAddr, followeeAddr) { + return followState[`${followerAddr}:${followeeAddr}`] || false + } + } +} + +function makeMemoFollow (profilePage) { + return { + async follow (addr) { + profilePage.followState = true + }, + async unfollow (addr) { + profilePage.followState = false } } } @@ -48,6 +63,15 @@ test('load throws when no memo db client is provided', async () => { ) }) +test('load throws when no address is provided', async () => { + const page = new ProfilePage({ memoDb: makeMemoDb({}) }) + + await assert.rejects( + () => page.load(), + /requires an address/ + ) +}) + test('load defaults limit to 100 and offset to 0', async () => { const calls = [] const addr = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' @@ -55,6 +79,9 @@ test('load defaults limit to 100 and offset to 0', async () => { async getPostsByAddr (a, params) { calls.push({ a, params }) return { posts: [], pagination: {} } + }, + async getFollowState () { + return false } } const page = new ProfilePage({ memoDb, addr }) @@ -69,6 +96,9 @@ test('load sets pagination to null when the API returns none', async () => { const memoDb = { async getPostsByAddr () { return { posts: [], pagination: undefined } + }, + async getFollowState () { + return false } } const page = new ProfilePage({ memoDb, addr }) @@ -78,3 +108,69 @@ test('load sets pagination to null when the API returns none', async () => { assert.equal(result.pagination, null) assert.equal(page.pagination, null) }) + +test('load fetches follow state when a viewer address is provided', async () => { + const myAddr = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' + const addr = 'bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy' + const memoDb = makeMemoDb({}, { [`${myAddr}:${addr}`]: true }) + const page = new ProfilePage({ memoDb, addr, myAddr }) + + const result = await page.load() + + assert.equal(result.followState, true) + assert.equal(page.isFollowing(), true) +}) + +test('isOwnProfile returns true when viewer address matches profile address', () => { + const addr = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' + const page = new ProfilePage({ memoDb: {}, addr, myAddr: addr }) + + assert.equal(page.isOwnProfile(), true) + assert.equal(page.canFollow(), false) +}) + +test('canFollow returns true when viewing another profile', () => { + const myAddr = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' + const addr = 'bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy' + const page = new ProfilePage({ memoDb: {}, addr, myAddr }) + + assert.equal(page.canFollow(), true) +}) + +test('follow delegates to the memo follow handler and updates state', async () => { + const myAddr = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' + const addr = 'bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy' + const memoDb = makeMemoDb({}) + const page = new ProfilePage({ memoDb, addr, myAddr }) + page.memoFollow = makeMemoFollow(page) + + const result = await page.follow() + + assert.equal(result.ok, true) + assert.equal(page.isFollowing(), true) +}) + +test('unfollow delegates to the memo follow handler and updates state', async () => { + const myAddr = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' + const addr = 'bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy' + const memoDb = makeMemoDb({}) + const page = new ProfilePage({ memoDb, addr, myAddr }) + page.memoFollow = makeMemoFollow(page) + page.followState = true + + const result = await page.unfollow() + + assert.equal(result.ok, true) + assert.equal(page.isFollowing(), false) +}) + +test('follow throws when no memo follow handler is injected', async () => { + const myAddr = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' + const addr = 'bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy' + const page = new ProfilePage({ memoDb: makeMemoDb({}), addr, myAddr }) + + await assert.rejects( + () => page.follow(), + /requires a memo follow handler/ + ) +}) diff --git a/psf-memo-client/test/unit/profiles.test.js b/psf-memo-client/test/unit/profiles.test.js index 8085ace..531a0bb 100644 --- a/psf-memo-client/test/unit/profiles.test.js +++ b/psf-memo-client/test/unit/profiles.test.js @@ -94,6 +94,46 @@ test('setAvatarUrl with no address does nothing', () => { assert.equal(profiles.getAvatarUrl(''), null) }) +test('setFollowState stores and getFollowState retrieves follow state', () => { + const profiles = new Profiles() + const selfAddr = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' + const targetAddr = 'bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy' + + profiles.setFollowState(selfAddr, targetAddr, true) + + assert.equal(profiles.getFollowState(selfAddr, targetAddr), true) +}) + +test('getFollowState returns false for an unknown follow relationship', () => { + const profiles = new Profiles() + const selfAddr = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' + const targetAddr = 'bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy' + + assert.equal(profiles.getFollowState(selfAddr, targetAddr), false) +}) + +test('setFollowState with no self address does nothing', () => { + const profiles = new Profiles() + const targetAddr = 'bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy' + + profiles.setFollowState('', targetAddr, true) + + assert.equal(profiles.getFollowState('', targetAddr), false) +}) + +test('follow state storage is independent per self address', () => { + const profiles = new Profiles() + const selfAddr = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' + const otherSelfAddr = 'bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a' + const targetAddr = 'bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy' + + profiles.setFollowState(selfAddr, targetAddr, true) + profiles.setFollowState(otherSelfAddr, targetAddr, false) + + assert.equal(profiles.getFollowState(selfAddr, targetAddr), true) + assert.equal(profiles.getFollowState(otherSelfAddr, targetAddr), false) +}) + test('avatar URL storage is independent of name and bio storage', () => { const profiles = new Profiles() const addr = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' diff --git a/psf-memo-db/acceptance/lib/handlers.js b/psf-memo-db/acceptance/lib/handlers.js index c636358..d015d70 100644 --- a/psf-memo-db/acceptance/lib/handlers.js +++ b/psf-memo-db/acceptance/lib/handlers.js @@ -15,6 +15,9 @@ import Adapters from '../../src/adapters/index.js' import ListRecentPosts from '../../src/use-cases/list-recent-posts.js' import ListPostsByAddr from '../../src/use-cases/list-posts-by-addr.js' import GetPostThread from '../../src/use-cases/get-post-thread.js' +import FollowState from '../../src/use-cases/follow-state.js' +import ListFollowing from '../../src/use-cases/list-following.js' +import ListFollowers from '../../src/use-cases/list-followers.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const tmpDir = path.resolve(__dirname, '..', '..', 'tmp', 'acceptance') @@ -82,6 +85,9 @@ async function createWorld () { const listRecentPosts = new ListRecentPosts({ adapters }) const listPostsByAddr = new ListPostsByAddr({ adapters }) const getPostThread = new GetPostThread({ adapters }) + const followState = new FollowState({ adapters }) + const listFollowing = new ListFollowing({ adapters }) + const listFollowers = new ListFollowers({ adapters }) let lastResponse = null @@ -90,6 +96,9 @@ async function createWorld () { listRecentPosts, listPostsByAddr, getPostThread, + followState, + listFollowing, + listFollowers, postHeightsIteratorCounter, postChildrenIteratorCounter, postsGetCounter, @@ -112,6 +121,11 @@ async function loadFixture (world, name) { return } + if (name === 'follows') { + await loadFollows(world) + return + } + if (name !== 'three-top-level-posts-and-one-reply') { throw new Error(`Unknown fixture: ${name}`) } @@ -197,6 +211,28 @@ async function loadPostsWithLikes (world) { } } +async function loadFollows (world) { + const follower1 = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' + const follower2 = 'bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a' + const followeeHash = 'cb481232299cd5743151ac4b2d63ae198e7bb0a9' + + const records = [ + { key: `${follower1}:${followeeHash}`, followerAddr: follower1, followeePkHash: followeeHash, unfollow: false }, + { key: `${follower2}:${followeeHash}`, followerAddr: follower2, followeePkHash: followeeHash, unfollow: false } + ] + + for (const record of records) { + await world.adapters.level.followsDb.put(record.key, { + followerAddr: record.followerAddr, + followeePkHash: record.followeePkHash, + unfollow: record.unfollow, + txid: `follow-${record.followerAddr.slice(-8)}-${record.followeePkHash.slice(-8)}`, + seen: Date.now(), + blockHeight: 600000 + }) + } +} + const handlers = [ { name: 'db instance with posts and postHeights stores', @@ -391,6 +427,84 @@ const handlers = [ throw new Error(`Expected exactly one postChildren scan, got ${calls}`) } } + }, + { + name: 'db instance with follows store', + pattern: /^a psf-memo-db instance with a follows store$/, + async run () { + // World is already created with the follows store. + } + }, + { + name: 'load fixture into follows store', + pattern: /^the fixture "(.+)" is loaded into the follows store$/, + async run (m, example, world) { + await loadFixture(world, m[1]) + } + }, + { + name: 'request follow state', + pattern: /^the client requests the follow state for follower (<[A-Za-z0-9_]+>) and followee (<[A-Za-z0-9_]+>)$/, + async run (m, example, world) { + const follower = resolveParam(m[1], example) + const followee = resolveParam(m[2], example) + const resp = await world.followState.execute({ followerAddr: follower, followeeAddr: followee }) + world.setLastResponse(resp) + } + }, + { + name: 'follow state reports following', + pattern: /^the follow state reports following ()$/, + run (m, example, world) { + const expected = resolveParam(m[1], example) === 'true' + const actual = world.getLastResponse().following + if (actual !== expected) { + throw new Error(`Expected following ${expected}, got ${actual}`) + } + } + }, + { + name: 'request following list', + pattern: /^the client requests the following list for (<[A-Za-z0-9_]+>)$/, + async run (m, example, world) { + const follower = resolveParam(m[1], example) + const resp = await world.listFollowing.execute({ followerAddr: follower }) + world.setLastResponse(resp) + } + }, + { + name: 'following list contains addresses', + pattern: /^the following list contains the addresses (<[A-Za-z0-9_]+>)$/, + run (m, example, world) { + const expected = resolveParam(m[1], example).split(',').map((s) => s.trim()).filter(Boolean) + const actual = world.getLastResponse().following + if (expected.join(',') !== actual.join(',')) { + throw new Error(`Expected following ${expected.join(',')}, got ${actual.join(',')}`) + } + } + }, + { + name: 'request followers list', + pattern: /^the client requests the followers list for (<[A-Za-z0-9_]+>)$/, + async run (m, example, world) { + const followee = resolveParam(m[1], example) + const resp = await world.listFollowers.execute({ followeeAddr: followee }) + world.setLastResponse(resp) + } + }, + { + name: 'followers list contains addresses', + pattern: /^the followers list contains the addresses (<[A-Za-z0-9_]+>)$/, + run (m, example, world) { + const raw = resolveParam(m[1], example).trim() + const expected = raw.length === 0 ? [] : raw.split(',').map((s) => s.trim()) + const actual = world.getLastResponse().followers + const expectedSet = new Set(expected) + const actualSet = new Set(actual) + if (expectedSet.size !== actualSet.size || !expectedSet.isSubsetOf(actualSet)) { + throw new Error(`Expected followers ${expected.join(',')}, got ${actual.join(',')}`) + } + } } ] diff --git a/psf-memo-db/package-lock.json b/psf-memo-db/package-lock.json index ca6b2b9..4bfeb7d 100644 --- a/psf-memo-db/package-lock.json +++ b/psf-memo-db/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@psf/bch-js": "7.1.11", "dotenv": "17.2.3", "kcors": "2.2.2", "koa": "2.13.1", @@ -42,6 +43,76 @@ "node": ">=18" } }, + "node_modules/@chris.troutner/bip32-utils": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@chris.troutner/bip32-utils/-/bip32-utils-1.0.5.tgz", + "integrity": "sha512-pa9dh5VpPmfol1bdLy+FyqONmxlf/QH6Q01a57OP6C9gTVOZM1Rt0kCLXxXKC6e2AnNIrXpYN1UtlyBm+r6P0g==", + "license": "MIT", + "dependencies": { + "keccak": "^3.0.1", + "tape": "*" + }, + "engines": { + "node": ">=10.15.1" + } + }, + "node_modules/@chris.troutner/retry-queue": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@chris.troutner/retry-queue/-/retry-queue-1.0.11.tgz", + "integrity": "sha512-IJt19IdG4oy20PnHvRHn1bPrMHCwzPuL2R8Ze13VbaKQXKYjcBV5neARn/TQJ/V2It1z2OkV0qj/sJV7scahaA==", + "license": "MIT", + "dependencies": { + "p-queue": "7.3.0", + "p-retry": "5.1.1" + } + }, + "node_modules/@chris.troutner/retry-queue-commonjs": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@chris.troutner/retry-queue-commonjs/-/retry-queue-commonjs-1.0.8.tgz", + "integrity": "sha512-jEHmCKffjIXTm0d/YcRQzCDPbzW2NFTEO72b1vmETnmATO57MmO39F37gEaq7J7lnF5JMxuM3Oq+FWa1L7HXxA==", + "license": "MIT", + "dependencies": { + "p-queue": "4.0.0", + "p-retry": "4.6.2" + } + }, + "node_modules/@chris.troutner/retry-queue-commonjs/node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/@chris.troutner/retry-queue-commonjs/node_modules/eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", + "license": "MIT" + }, + "node_modules/@chris.troutner/retry-queue-commonjs/node_modules/p-queue": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-4.0.0.tgz", + "integrity": "sha512-3cRXXn3/O0o3+eVmUroJPSj/esxoEFIm0ZOno/T+NzG/VZgPOqQ8WKmlNqubSEpZmCIngEy34unkHGg83ZIBmg==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^3.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@chris.troutner/retry-queue-commonjs/node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@colors/colors": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", @@ -66,7 +137,6 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -79,7 +149,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -96,7 +165,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -113,7 +181,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -130,7 +197,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -147,7 +213,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -164,7 +229,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -181,7 +245,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -198,7 +261,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -215,7 +277,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -232,7 +293,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -249,7 +309,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -266,7 +325,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -283,7 +341,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -300,7 +357,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -317,7 +373,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -334,7 +389,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -351,7 +405,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -368,7 +421,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -385,7 +437,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -402,7 +453,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -419,7 +469,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -436,7 +485,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -768,7 +816,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -779,7 +826,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -789,7 +835,6 @@ "version": "0.3.11", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -800,20 +845,72 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@ljharb/now": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@ljharb/now/-/now-1.0.1.tgz", + "integrity": "sha512-yRQ3B8l2v4vTLQ2l+4kBu6EsXaX7czuLB7fvUz0vkHHziHje75R2t/0Mm/vIo31tlZVCEvCoXHkNKW+ugv3uSA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@ljharb/resumer": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@ljharb/resumer/-/resumer-0.1.3.tgz", + "integrity": "sha512-d+tsDgfkj9X5QTriqM4lKesCkMMJC3IrbPKHvayP00ELx2axdXvDfWkqjxrLXIzGcQzmj7VAUT1wopqARTvafw==", + "license": "MIT", + "dependencies": { + "@ljharb/through": "^2.3.13", + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@ljharb/through": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.14.tgz", + "integrity": "sha512-ajBvlKpWucBB17FuQYUShqpqy8GRgYEpJW0vWJbUu1CV9lWyrDCapy0lScU8T8Z6qn49sSwJB3+M+evYIdGg+A==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -860,6 +957,97 @@ "node": ">=14" } }, + "node_modules/@psf/bch-js": { + "version": "7.1.11", + "resolved": "https://registry.npmjs.org/@psf/bch-js/-/bch-js-7.1.11.tgz", + "integrity": "sha512-gDJCaY2aG8EtUWcR9D4tbiyM1BkHd7gkuO7NRaLpwdbSrnU51lyPr69mjQLKOcVlL+aXdJsOjL5pm15t4zq18w==", + "license": "MIT", + "dependencies": { + "@chris.troutner/bip32-utils": "1.0.5", + "@psf/bip21": "2.0.1", + "@psf/bitcoincashjs-lib": "4.0.3", + "@psf/coininfo": "4.0.0", + "axios": "1.13.2", + "bc-bip68": "1.0.5", + "bchaddrjs-slp": "0.2.14", + "bigi": "1.4.2", + "bignumber.js": "9.3.1", + "bip-schnorr": "0.6.7", + "bip38": "3.1.1", + "bip39": "3.1.0", + "bip66": "1.1.5", + "bitcoinjs-message": "2.2.0", + "bs58": "4.0.1", + "ecashaddrjs": "1.0.7", + "ini": "6.0.0", + "randombytes": "2.1.0", + "safe-buffer": "5.2.1", + "satoshi-bitcoin": "1.0.5", + "slp-mdm": "0.0.7", + "slp-parser": "0.0.4", + "wif": "2.0.6", + "x402-bch-axios": "2.2.1" + } + }, + "node_modules/@psf/bip21": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@psf/bip21/-/bip21-2.0.1.tgz", + "integrity": "sha512-U9c8xBV31n+D7qxOPBO0vQ015DNvKskWCUbVgoMfH5AUNHLYrSDWIrCx4P7v9etfdu6LpPdsYr53KDSAIk0b7Q==", + "license": "ISC", + "dependencies": { + "qs": "^6.3.0" + } + }, + "node_modules/@psf/bitcoincash-ops": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@psf/bitcoincash-ops/-/bitcoincash-ops-2.0.0.tgz", + "integrity": "sha512-M3PWqRpeJq6rli2NqWGbas76z9TrdGOmNuDFACBWBMctPucEAsFQY2AmyFHRSa7hEwythwvrPh9AG/n6ehmEog==", + "license": "MIT" + }, + "node_modules/@psf/bitcoincashjs-lib": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@psf/bitcoincashjs-lib/-/bitcoincashjs-lib-4.0.3.tgz", + "integrity": "sha512-sJYi7jYUqR7S+Z8TjN3W/5Lcju7xcIBxYLhIEpnOF+jR9kMt0ftpVjQBt8vVsJZUD8ArL9bf59oDETjdOUcdGQ==", + "license": "MIT", + "dependencies": { + "@psf/bitcoincash-ops": "^2.0.0", + "@psf/pushdata-bitcoin": "^1.2.2", + "bech32": "^1.1.2", + "bigi": "^1.4.0", + "bip66": "^1.1.0", + "bs58check": "^2.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.3", + "ecurve": "^1.0.0", + "merkle-lib": "^2.0.10", + "randombytes": "^2.0.1", + "safe-buffer": "^5.0.1", + "typeforce": "^1.18.0", + "varuint-bitcoin": "^1.0.4", + "wif": "^2.0.1" + }, + "engines": { + "node": ">=10.15.1" + } + }, + "node_modules/@psf/coininfo": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@psf/coininfo/-/coininfo-4.0.0.tgz", + "integrity": "sha512-RwBc09790kbaOt8uZJMyvLqf1UziTd20FXu78bM8bMlkClnZQTJyNDdLCsFSBkJQYAJtGMkjdQ/o3/UaSC7c2Q==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.1" + } + }, + "node_modules/@psf/pushdata-bitcoin": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@psf/pushdata-bitcoin/-/pushdata-bitcoin-1.2.2.tgz", + "integrity": "sha512-e1qkZLJFU6Ldg7TMBgSkiR5U1NfpRgSIr2ppk8BeED/Q9wUc9DEVAjXfPMD65xgXdqnlyFEtLbpPMWwyydMVUQ==", + "license": "MIT", + "dependencies": { + "@psf/bitcoincash-ops": "^2.0.0" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -931,7 +1119,6 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, "license": "MIT" }, "node_modules/@types/istanbul-lib-coverage": { @@ -945,7 +1132,6 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, "license": "MIT" }, "node_modules/@types/json5": { @@ -959,12 +1145,17 @@ "version": "25.9.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, + "node_modules/@types/retry": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", + "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", + "license": "MIT" + }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", @@ -989,7 +1180,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", @@ -1000,28 +1190,24 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", @@ -1033,14 +1219,12 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -1053,7 +1237,6 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, "license": "MIT", "dependencies": { "@xtuc/ieee754": "^1.2.0" @@ -1063,7 +1246,6 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@xtuc/long": "4.2.2" @@ -1073,14 +1255,12 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -1097,7 +1277,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -1111,7 +1290,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -1124,7 +1302,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -1139,7 +1316,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", @@ -1150,7 +1326,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", "integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==", - "dev": true, "license": "MIT", "peerDependencies": { "webpack": "4.x.x || 5.x.x", @@ -1161,7 +1336,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.5.0.tgz", "integrity": "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==", - "dev": true, "license": "MIT", "dependencies": { "envinfo": "^7.7.3" @@ -1174,7 +1348,6 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz", "integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==", - "dev": true, "license": "MIT", "peerDependencies": { "webpack-cli": "4.x.x" @@ -1189,14 +1362,12 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, "license": "Apache-2.0" }, "node_modules/abstract-leveldown": { @@ -1234,7 +1405,6 @@ "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -1247,7 +1417,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.13.0" @@ -1287,7 +1456,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -1305,7 +1473,6 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -1322,7 +1489,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, "license": "MIT" }, "node_modules/ansi-colors": { @@ -1367,7 +1533,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -1436,14 +1601,12 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -1479,6 +1642,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array.prototype.every": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/array.prototype.every/-/array.prototype.every-1.1.7.tgz", + "integrity": "sha512-BIP72rKvrKd08ptbetLb4qvrlGjkv30yOKgKcTtOIbHyQt3shr/jyOzdApiCOh3LPYrpJo5M6i0zmVldOF2pUw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "is-string": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.findlast": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", @@ -1545,7 +1727,6 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -1581,7 +1762,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", @@ -1619,17 +1799,21 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" @@ -1641,13 +1825,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, + "node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1672,7 +1875,6 @@ "version": "2.10.33", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==", - "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -1681,11 +1883,374 @@ "node": ">=6.0.0" } }, + "node_modules/bc-bip68": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/bc-bip68/-/bc-bip68-1.0.5.tgz", + "integrity": "sha512-GzaMlN7pNthrY5BhReVhnfr4Ixx+GUSfyNRHYh0QiMUF0d0+0YaD8MpEdv6AjFBksg/zlqL1fVCBBm6PpTt2Rg==", + "license": "ISC", + "engines": { + "node": ">=4.5.0" + } + }, + "node_modules/bch-consumer": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/bch-consumer/-/bch-consumer-1.6.2.tgz", + "integrity": "sha512-PclXVzmWtwqxe2ba6Mo9QbqxJ+5rUog8c+Ro5PV/W3IxkKNotVRa71bLei99AIELDO3A3ljfXkoCNsSM7tR37A==", + "license": "MIT", + "dependencies": { + "@psf/bch-js": "6.7.3", + "apidoc": "0.51.0", + "axios": "0.25.0" + } + }, + "node_modules/bch-consumer/node_modules/@psf/bch-js": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@psf/bch-js/-/bch-js-6.7.3.tgz", + "integrity": "sha512-z6oJvPAXxSObPhqUAxCkCuPL2bp3Bs1oyhBZpQ5SR6cbJzcOMhuI4bHho2srH5agSCU+bbjehBMRGvo18eGW0A==", + "license": "MIT", + "dependencies": { + "@chris.troutner/bip32-utils": "1.0.5", + "@psf/bip21": "2.0.1", + "@psf/bitcoincash-ops": "2.0.0", + "@psf/bitcoincashjs-lib": "4.0.3", + "@psf/coininfo": "4.0.0", + "axios": "0.26.1", + "bc-bip68": "1.0.5", + "bchaddrjs-slp": "0.2.5", + "bigi": "1.4.2", + "bignumber.js": "9.0.0", + "bip-schnorr": "0.3.0", + "bip38": "2.0.2", + "bip39": "3.0.2", + "bip66": "1.1.5", + "bitcoinjs-message": "2.0.0", + "bs58": "4.0.1", + "ecashaddrjs": "1.0.7", + "ini": "1.3.8", + "randombytes": "2.0.6", + "safe-buffer": "5.1.2", + "satoshi-bitcoin": "1.0.4", + "slp-mdm": "0.0.6", + "slp-parser": "0.0.4", + "wif": "2.0.6" + } + }, + "node_modules/bch-consumer/node_modules/@psf/bch-js/node_modules/axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.8" + } + }, + "node_modules/bch-consumer/node_modules/@types/node": { + "version": "11.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", + "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==", + "license": "MIT" + }, + "node_modules/bch-consumer/node_modules/apidoc": { + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/apidoc/-/apidoc-0.51.0.tgz", + "integrity": "sha512-3P4srhm6NA+kE/YRM4qL5jESElpQQJL+Z8n7hyr4uC+1DSwGzmE6adXPVxuT/hpDyShrqmRlHk8hf40RSqEsNw==", + "license": "MIT", + "os": [ + "darwin", + "freebsd", + "linux", + "openbsd", + "win32" + ], + "dependencies": { + "bootstrap": "3.4.1", + "commander": "^8.3.0", + "diff-match-patch": "^1.0.5", + "esbuild-loader": "^2.16.0", + "expose-loader": "^3.1.0", + "fs-extra": "^10.0.0", + "glob": "^7.2.0", + "handlebars": "^4.7.7", + "iconv-lite": "^0.6.3", + "jquery": "^3.6.0", + "klaw-sync": "^6.0.0", + "lodash": "^4.17.21", + "markdown-it": "^12.2.0", + "nodemon": "^2.0.15", + "path-to-regexp": "^6.2.0", + "prismjs": "^1.25.0", + "semver": "^7.3.5", + "style-loader": "^3.3.1", + "url-parse": "^1.5.3", + "webpack": "^5.64.2", + "webpack-cli": "^4.9.1", + "winston": "^3.3.3" + }, + "bin": { + "apidoc": "bin/apidoc" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/bch-consumer/node_modules/axios": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz", + "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.7" + } + }, + "node_modules/bch-consumer/node_modules/bchaddrjs-slp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/bchaddrjs-slp/-/bchaddrjs-slp-0.2.5.tgz", + "integrity": "sha512-33flmPcqMFswerKu7477DSUNMVMQR3tHDk3lvbmsdkEva+TxVGGWWE/p5Lqx9M/8t3vkbe7fzmVhj4QhChcCyA==", + "license": "MIT", + "dependencies": { + "bs58check": "^2.1.2", + "cashaddrjs-slp": "^0.2.11" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/bch-consumer/node_modules/big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bch-consumer/node_modules/bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bch-consumer/node_modules/bip-schnorr": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/bip-schnorr/-/bip-schnorr-0.3.0.tgz", + "integrity": "sha512-Sc1Hn2+1n+okPEW8G+JLjeaM12dsUOwr+oFlMDSKR9wYwNGMw0alskeBIHTmXxBxMZSWKhCW7PwKQVDyGmnaVg==", + "license": "MIT", + "dependencies": { + "ecurve": "^1.0.6", + "js-sha256": "^0.9.0", + "random-bytes": "^1.0.0", + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/bch-consumer/node_modules/bip38": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bip38/-/bip38-2.0.2.tgz", + "integrity": "sha512-22KDak0RDyghFbR0Si7wyq9IgY423YzGYzWLpGeofH3DaolOQqjD3mNN08eFoubKlbyclOQKFwtONMv2SD9V3A==", + "dependencies": { + "bigi": "^1.2.0", + "browserify-aes": "^1.0.1", + "bs58check": "<3.0.0", + "buffer-xor": "^1.0.2", + "create-hash": "^1.1.1", + "ecurve": "^1.0.0", + "scryptsy": "^2.0.0" + } + }, + "node_modules/bch-consumer/node_modules/bip39": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.2.tgz", + "integrity": "sha512-J4E1r2N0tUylTKt07ibXvhpT2c5pyAFgvuA5q1H9uDy6dEGpjV8jmymh3MTYJDLCNbIVClSB9FbND49I6N24MQ==", + "license": "ISC", + "dependencies": { + "@types/node": "11.11.6", + "create-hash": "^1.1.0", + "pbkdf2": "^3.0.9", + "randombytes": "^2.0.1" + } + }, + "node_modules/bch-consumer/node_modules/bitcoinjs-message": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bitcoinjs-message/-/bitcoinjs-message-2.0.0.tgz", + "integrity": "sha512-H5pJC7/eSqVjREiEOZ4jifX+7zXYP3Y28GIOIqg9hrgE7Vj8Eva9+HnVqnxwA1rJPOwZKuw0vo6k0UxgVc6q1A==", + "license": "MIT", + "dependencies": { + "bs58check": "^2.0.2", + "buffer-equals": "^1.0.3", + "create-hash": "^1.1.2", + "secp256k1": "^3.0.1", + "varuint-bitcoin": "^1.0.1" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/bch-consumer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bch-consumer/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/bch-consumer/node_modules/randombytes": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", + "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/bch-consumer/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/bch-consumer/node_modules/satoshi-bitcoin": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/satoshi-bitcoin/-/satoshi-bitcoin-1.0.4.tgz", + "integrity": "sha512-YuHOmw5wsz6wuHIQdsz5b2cgtuKtV/jEcZ4NGmWN5tM/gz5T9Q+DSuLlnuf5BP/jsQWgR0ofmY4f8Dm6JnqSug==", + "license": "MIT", + "dependencies": { + "big.js": "^3.1.3" + } + }, + "node_modules/bch-consumer/node_modules/slp-mdm": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/slp-mdm/-/slp-mdm-0.0.6.tgz", + "integrity": "sha512-fbjlIg/o8OtzgK2JydC6POJp3Qup/rLgy4yB5hoLgxWRlERyJyE29ScwS3r9TTwPxe12qK55pyivAdNOZZXL0A==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/bch-donation": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/bch-donation/-/bch-donation-1.1.2.tgz", + "integrity": "sha512-V7xQ23M6Ocavb1Nj4/pATwaLX/uF/9s2nWir3f+F3aetD3kMmsRHFRuXJwGQG3dSC5fJtRR/M5Z2zynY3t91Xg==", + "license": "MIT", + "dependencies": { + "apidoc": "0.51.0" + } + }, + "node_modules/bch-donation/node_modules/apidoc": { + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/apidoc/-/apidoc-0.51.0.tgz", + "integrity": "sha512-3P4srhm6NA+kE/YRM4qL5jESElpQQJL+Z8n7hyr4uC+1DSwGzmE6adXPVxuT/hpDyShrqmRlHk8hf40RSqEsNw==", + "license": "MIT", + "os": [ + "darwin", + "freebsd", + "linux", + "openbsd", + "win32" + ], + "dependencies": { + "bootstrap": "3.4.1", + "commander": "^8.3.0", + "diff-match-patch": "^1.0.5", + "esbuild-loader": "^2.16.0", + "expose-loader": "^3.1.0", + "fs-extra": "^10.0.0", + "glob": "^7.2.0", + "handlebars": "^4.7.7", + "iconv-lite": "^0.6.3", + "jquery": "^3.6.0", + "klaw-sync": "^6.0.0", + "lodash": "^4.17.21", + "markdown-it": "^12.2.0", + "nodemon": "^2.0.15", + "path-to-regexp": "^6.2.0", + "prismjs": "^1.25.0", + "semver": "^7.3.5", + "style-loader": "^3.3.1", + "url-parse": "^1.5.3", + "webpack": "^5.64.2", + "webpack-cli": "^4.9.1", + "winston": "^3.3.3" + }, + "bin": { + "apidoc": "bin/apidoc" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/bch-donation/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bchaddrjs-slp": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/bchaddrjs-slp/-/bchaddrjs-slp-0.2.14.tgz", + "integrity": "sha512-4EdUV6Kwu2Ae8QyYtLAnHOUu0Zfl0w5wDPbeftiu3+fVr6ZyPmuVKlZtU2HERx76BOngZpGTf+suM3nuPJ0Tuw==", + "license": "MIT", + "dependencies": { + "bs58check": "^2.1.2", + "cashaddrjs-slp": "^0.2.12" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", + "license": "MIT" + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bigi": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/bigi/-/bigi-1.4.2.tgz", + "integrity": "sha512-ddkU+dFIuEIW8lE7ZwdIAf2UPoM90eaprg5m3YXAVVTmKlqV/9BX4A2M8BOK2yOq6/VgZFVhK6QAxJebhlbhzw==" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", "license": "MIT", "engines": { "node": "*" @@ -1695,7 +2260,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -1704,12 +2268,98 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bip-schnorr": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/bip-schnorr/-/bip-schnorr-0.6.7.tgz", + "integrity": "sha512-Pf1o+whA52l7NC33CZY4eRtcB+dUCT54hKCF8mnw2349CG89LOXODHRWmUEcSSWzoi+Kjg1CtMmw6uV43tmJFA==", + "license": "MIT", + "dependencies": { + "bigi": "^1.4.2", + "ecurve": "^1.0.6", + "js-sha256": "^0.9.0", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/bip38": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bip38/-/bip38-3.1.1.tgz", + "integrity": "sha512-d5AQWuXS95rdrEBnaKeZ9osMkvDR3QDULbAfoE7xRw0W8jfDrza9CsZwf+UhqjV6lbbfaWCrJIu5uObJlfo5Rw==", + "dependencies": { + "bigi": "^1.2.0", + "browserify-aes": "^1.0.1", + "bs58check": "<3.0.0", + "buffer-xor": "^1.0.2", + "create-hash": "^1.1.1", + "ecurve": "^1.0.0", + "safe-buffer": "~5.1.1", + "scryptsy": "^2.1.0" + } + }, + "node_modules/bip38/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/bip39": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz", + "integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==", + "license": "ISC", + "dependencies": { + "@noble/hashes": "^1.2.0" + } + }, + "node_modules/bip66": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", + "integrity": "sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/bitcoinjs-message": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bitcoinjs-message/-/bitcoinjs-message-2.2.0.tgz", + "integrity": "sha512-103Wy3xg8Y9o+pdhGP4M3/mtQQuUWs6sPuOp1mYphSUoSMHjHTlkj32K4zxU8qMH0Ckv23emfkGlFWtoWZ7YFA==", + "license": "MIT", + "dependencies": { + "bech32": "^1.1.3", + "bs58check": "^2.1.2", + "buffer-equals": "^1.0.3", + "create-hash": "^1.1.2", + "secp256k1": "^3.0.1", + "varuint-bitcoin": "^1.0.1" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/bn.js": { + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz", + "integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==", + "license": "MIT" + }, "node_modules/bootstrap": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-3.4.1.tgz", "integrity": "sha512-yN5oZVmRCwe5aKwzRj6736nSmKDX7pLYwsXiCj/EYmo16hODaBiT4En5btW/jhBF/seV+XMx3aYwukYC3A49DA==", "deprecated": "This version of Bootstrap is no longer supported. Please upgrade to the latest version.", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -1737,6 +2387,12 @@ "node": ">=8" } }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT" + }, "node_modules/browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -1744,11 +2400,24 @@ "dev": true, "license": "ISC" }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "license": "MIT", + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/browserslist": { "version": "4.28.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -1778,6 +2447,26 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/bs58check": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", + "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", + "license": "MIT", + "dependencies": { + "bs58": "^4.0.0", + "create-hash": "^1.1.0", + "safe-buffer": "^5.1.2" + } + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -1802,11 +2491,25 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-equals": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/buffer-equals/-/buffer-equals-1.0.4.tgz", + "integrity": "sha512-99MsCq0j5+RhubVEtKQgKaD6EM+UP3xJgIvQqwJ3SOLDUekzxMX1ylXBng+Wa2sh7mGT0W6RUly8ojjr1Tt6nA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, + "license": "MIT" + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", "license": "MIT" }, "node_modules/builtins": { @@ -1879,7 +2582,6 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -1950,7 +2652,6 @@ "version": "1.0.30001793", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", - "dev": true, "funding": [ { "type": "opencollective", @@ -1967,6 +2668,15 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cashaddrjs-slp": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/cashaddrjs-slp/-/cashaddrjs-slp-0.2.12.tgz", + "integrity": "sha512-n2TTIuW6vZZxYvjvsUAA+wOM0Zkj+3RRKUtDC1XSu4Ic4XVr0yFJkl1bzQkHWda7nkVT51sxjZneygz7D0SyrQ==", + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.34" + } + }, "node_modules/catering": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/catering/-/catering-2.1.1.tgz", @@ -2046,7 +2756,6 @@ "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, "funding": [ { "type": "individual", @@ -2074,12 +2783,25 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0" } }, + "node_modules/cipher-base": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2099,7 +2821,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, "license": "MIT", "dependencies": { "is-plain-object": "^2.0.4", @@ -2210,14 +2931,24 @@ "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "dev": true, "license": "MIT", "engines": { "node": ">= 12" @@ -2227,7 +2958,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/content-disposition": { @@ -2277,6 +3007,39 @@ "integrity": "sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2291,11 +3054,17 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.0.0.tgz", + "integrity": "sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg==", + "deprecated": "Active development of CryptoJS has been discontinued. This library is no longer maintained.", + "license": "MIT" + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -2313,7 +3082,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -2331,7 +3099,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -2411,7 +3178,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -2429,7 +3195,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", @@ -2443,6 +3208,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defined": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", + "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -2482,7 +3265,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", - "dev": true, "license": "Apache-2.0" }, "node_modules/doctrine": { @@ -2510,6 +3292,54 @@ "url": "https://dotenvx.com" } }, + "node_modules/dotignore": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dotignore/-/dotignore-0.1.2.tgz", + "integrity": "sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw==", + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.4" + }, + "bin": { + "ignored": "bin/ignored" + } + }, + "node_modules/dotignore/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dotignore/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/drbg.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", + "integrity": "sha512-F4wZ06PvqxYLFEZKkFxTDcns9oFNk34hvmJSEwdzsxVQ8YI5YaxtACgQatkYgv2VI2CFkUd2Y+xosPQnHv809g==", + "license": "MIT", + "dependencies": { + "browserify-aes": "^1.0.6", + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2531,6 +3361,34 @@ "dev": true, "license": "MIT" }, + "node_modules/ecashaddrjs": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/ecashaddrjs/-/ecashaddrjs-1.0.7.tgz", + "integrity": "sha512-KsvHYLlYtLr/GBkEPiwwQDIDBzqRx61qC34n1puHKOjVE4Uwg3syHccjFCqNynLa6T6xI0Rd7ByCRUJcuJcoIw==", + "license": "MIT", + "dependencies": { + "big-integer": "1.6.36" + } + }, + "node_modules/ecashaddrjs/node_modules/big-integer": { + "version": "1.6.36", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.36.tgz", + "integrity": "sha512-t70bfa7HYEA1D9idDbmuv7YbsbVkQ+Hp+8KFSul4aE5e/i1bjCNIRYJZlA8Q8p0r9T8cF/RVvwUgRA//FydEyg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ecurve": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/ecurve/-/ecurve-1.0.6.tgz", + "integrity": "sha512-/BzEjNfiSuB7jIWKcS/z8FK9jNjmEWvUV2YZ4RLSmcDtP7Lq0m6FvDuSnJpBlDpGRpfRQeTLGLBI8H+kEv0r+w==", + "license": "MIT", + "dependencies": { + "bigi": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2541,9 +3399,23 @@ "version": "1.5.366", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.366.tgz", "integrity": "sha512-OlRuhb688YTCzzU3gXPLn6nGyd+F+53INE1qaKKlu6kETErE8FYsyDh0XqXEU+uBRn0MpCzz2vfNwORhkap8qg==", - "dev": true, "license": "ISC" }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -2555,7 +3427,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -2596,7 +3467,6 @@ "version": "5.22.2", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.2.tgz", "integrity": "sha512-0rxICaFZ7NQho/sHely2bvOPRP0Eu2B0NZ9zM54YvRvWMn7jfz3DmnOZDR9LlXDdDcqntAVc6Hfy4gr/tdH/Ag==", - "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -2610,7 +3480,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", - "dev": true, "license": "BSD-2-Clause", "funding": { "url": "https://github.com/fb55/entities?sponsor=1" @@ -2620,7 +3489,6 @@ "version": "7.21.0", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", - "dev": true, "license": "MIT", "bin": { "envinfo": "dist/cli.js" @@ -2643,7 +3511,6 @@ "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", - "dev": true, "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.2", @@ -2726,6 +3593,26 @@ "node": ">= 0.4" } }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-iterator-helpers": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz", @@ -2758,7 +3645,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", - "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { @@ -2777,7 +3663,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2793,7 +3678,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -2806,7 +3690,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.2.7", @@ -2824,7 +3707,6 @@ "version": "0.16.17", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.17.tgz", "integrity": "sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -2862,7 +3744,6 @@ "version": "2.21.0", "resolved": "https://registry.npmjs.org/esbuild-loader/-/esbuild-loader-2.21.0.tgz", "integrity": "sha512-k7ijTkCT43YBSZ6+fBCW1Gin7s46RrJ0VQaM8qA7lq7W+OLsGgtLyFV8470FzYi/4TeDexniTBTPTwZUnXXR5g==", - "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.16.17", @@ -2883,7 +3764,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -2896,7 +3776,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3624,7 +4503,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -3637,7 +4515,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -3653,16 +4530,31 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.x" } }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "license": "MIT", + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -3696,7 +4588,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-3.1.0.tgz", "integrity": "sha512-2RExSo0yJiqP+xiUue13jQa2IHE8kLDzTI7b6kn+vUlBVvlzNSiLDzo4e5Pp5J039usvTUnxZ8sUOhv0Kg15NA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 12.13.0" @@ -3713,7 +4604,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -3750,7 +4640,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", - "dev": true, "funding": [ { "type": "github", @@ -3767,7 +4656,6 @@ "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 4.9.1" @@ -3810,6 +4698,12 @@ "moment": "^2.11.2" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3843,7 +4737,6 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, "license": "BSD-3-Clause", "bin": { "flat": "cli.js" @@ -3877,11 +4770,30 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", "license": "MIT" }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.2.7" @@ -3910,6 +4822,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -3923,7 +4851,6 @@ "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -3938,14 +4865,12 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -3969,7 +4894,6 @@ "version": "1.1.8", "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -3990,7 +4914,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4049,6 +4972,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -4091,7 +5023,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -4110,7 +5041,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -4143,14 +5073,12 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, "license": "BSD-2-Clause" }, "node_modules/glob/node_modules/brace-expansion": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -4161,7 +5089,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -4190,7 +5117,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, "license": "MIT", "dependencies": { "define-properties": "^1.2.1", @@ -4219,7 +5145,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -4233,7 +5158,6 @@ "version": "4.7.9", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", - "dev": true, "license": "MIT", "dependencies": { "minimist": "^1.2.5", @@ -4255,7 +5179,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4264,11 +5187,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-dynamic-import": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/has-dynamic-import/-/has-dynamic-import-2.1.1.tgz", + "integrity": "sha512-DuTCn6K/RW8S27npDMumGKsjG6HE7MxzedZka5tJP+9dqfxks+UMqKBmeCijHtIhsBEZPlbMg0qMHi2nKYVtKQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4278,7 +5217,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -4291,7 +5229,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.0" @@ -4330,6 +5267,73 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash-base": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/hash-base/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/hash-base/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hash-base/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/hash-base/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/hash-base/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "node_modules/hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", @@ -4352,6 +5356,17 @@ "he": "bin/he" } }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4457,7 +5472,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true, "license": "ISC" }, "node_modules/import-fresh": { @@ -4481,7 +5495,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, "license": "MIT", "dependencies": { "pkg-dir": "^4.2.0", @@ -4521,7 +5534,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -4534,11 +5546,19 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4553,17 +5573,31 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -4588,7 +5622,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, "license": "MIT", "dependencies": { "async-function": "^1.0.0", @@ -4608,7 +5641,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, "license": "MIT", "dependencies": { "has-bigints": "^1.0.2" @@ -4624,7 +5656,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -4637,7 +5668,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -4677,7 +5707,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4690,7 +5719,6 @@ "version": "2.16.2", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.3" @@ -4706,7 +5734,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -4724,7 +5751,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -4750,7 +5776,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -4807,7 +5832,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4820,7 +5844,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4842,7 +5865,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -4879,7 +5901,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, "license": "MIT", "dependencies": { "isobject": "^3.0.1" @@ -4910,7 +5931,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4923,7 +5943,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -4951,7 +5970,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -4968,7 +5986,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -4986,7 +6003,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" @@ -5015,7 +6031,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5028,7 +6043,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3" @@ -5044,7 +6058,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -5061,7 +6074,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, "license": "MIT" }, "node_modules/isexe": { @@ -5074,7 +6086,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5157,7 +6168,6 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -5172,7 +6182,6 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -5188,7 +6197,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -5198,7 +6206,12 @@ "version": "3.7.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", - "dev": true, + "license": "MIT" + }, + "node_modules/js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==", "license": "MIT" }, "node_modules/js-tokens": { @@ -5266,7 +6279,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" @@ -5307,6 +6319,21 @@ "node": ">= 6.0.0" } }, + "node_modules/keccak": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.4.tgz", + "integrity": "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/keygrip": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", @@ -5333,7 +6360,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5343,7 +6369,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", - "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.1.11" @@ -5740,7 +6765,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", - "dev": true, "license": "MIT", "dependencies": { "uc.micro": "^1.0.1" @@ -5777,7 +6801,6 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.11.5" @@ -5791,7 +6814,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dev": true, "license": "MIT", "dependencies": { "big.js": "^5.2.2", @@ -5806,7 +6828,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -5835,7 +6856,6 @@ "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true, "license": "MIT" }, "node_modules/lodash.get": { @@ -5992,7 +7012,6 @@ "version": "12.3.2", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1", @@ -6014,11 +7033,21 @@ "node": ">= 0.4" } }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "license": "MIT", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, "node_modules/mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", - "dev": true, "license": "MIT" }, "node_modules/media-typer": { @@ -6045,6 +7074,12 @@ "node": ">= 8" } }, + "node_modules/merkle-lib": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/merkle-lib/-/merkle-lib-2.0.10.tgz", + "integrity": "sha512-XrNQvUbn1DL5hKNe46Ccs+Tu3/PYOlrcZILuGUhb95oKBPjc/nmIC8D462PQkipVDGKRvwhn+QFg2cCdIvmDJA==", + "license": "MIT" + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -6097,6 +7132,371 @@ "node": ">=6" } }, + "node_modules/minimal-slp-wallet": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/minimal-slp-wallet/-/minimal-slp-wallet-7.0.5.tgz", + "integrity": "sha512-yrWQwXrtUZYvB6mWe3HTBzQGK1oEjgHuXkanx94tejPR3N8nJLmRRMQu27oWIYD/m3wm0wU8UreYXPIA0iN/iw==", + "license": "MIT", + "dependencies": { + "@chris.troutner/retry-queue": "1.0.11", + "@psf/bch-js": "7.1.4", + "bch-consumer": "1.6.2", + "bch-donation": "1.1.2", + "crypto-js": "4.0.0" + } + }, + "node_modules/minimal-slp-wallet/node_modules/@psf/bch-js": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/@psf/bch-js/-/bch-js-7.1.4.tgz", + "integrity": "sha512-/+5mbg6RFybKPNtB/m0NEOJYVyT4Rosy/GHGjHknOEkBRF2Xd0/pZTtPAd+BtvzePDki4c2KyL13VkF5XghFyw==", + "license": "MIT", + "dependencies": { + "@chris.troutner/bip32-utils": "1.0.5", + "@psf/bip21": "2.0.1", + "@psf/bitcoincashjs-lib": "4.0.3", + "@psf/coininfo": "4.0.0", + "axios": "1.13.2", + "bc-bip68": "1.0.5", + "bchaddrjs-slp": "0.2.14", + "bigi": "1.4.2", + "bignumber.js": "9.3.1", + "bip-schnorr": "0.6.7", + "bip38": "3.1.1", + "bip39": "3.1.0", + "bip66": "1.1.5", + "bitcoinjs-message": "2.2.0", + "bs58": "4.0.1", + "ecashaddrjs": "1.0.7", + "ini": "6.0.0", + "randombytes": "2.1.0", + "safe-buffer": "5.2.1", + "satoshi-bitcoin": "1.0.5", + "slp-mdm": "0.0.7", + "slp-parser": "0.0.4", + "wif": "2.0.6", + "x402-bch-axios": "2.1.0" + } + }, + "node_modules/minimal-slp-wallet/node_modules/@types/node": { + "version": "11.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", + "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==", + "license": "MIT" + }, + "node_modules/minimal-slp-wallet/node_modules/apidoc": { + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/apidoc/-/apidoc-0.51.0.tgz", + "integrity": "sha512-3P4srhm6NA+kE/YRM4qL5jESElpQQJL+Z8n7hyr4uC+1DSwGzmE6adXPVxuT/hpDyShrqmRlHk8hf40RSqEsNw==", + "license": "MIT", + "os": [ + "darwin", + "freebsd", + "linux", + "openbsd", + "win32" + ], + "dependencies": { + "bootstrap": "3.4.1", + "commander": "^8.3.0", + "diff-match-patch": "^1.0.5", + "esbuild-loader": "^2.16.0", + "expose-loader": "^3.1.0", + "fs-extra": "^10.0.0", + "glob": "^7.2.0", + "handlebars": "^4.7.7", + "iconv-lite": "^0.6.3", + "jquery": "^3.6.0", + "klaw-sync": "^6.0.0", + "lodash": "^4.17.21", + "markdown-it": "^12.2.0", + "nodemon": "^2.0.15", + "path-to-regexp": "^6.2.0", + "prismjs": "^1.25.0", + "semver": "^7.3.5", + "style-loader": "^3.3.1", + "url-parse": "^1.5.3", + "webpack": "^5.64.2", + "webpack-cli": "^4.9.1", + "winston": "^3.3.3" + }, + "bin": { + "apidoc": "bin/apidoc" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/minimal-slp-wallet/node_modules/big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/minimal-slp-wallet/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/minimal-slp-wallet/-/minimal-slp-wallet-7.0.2.tgz", + "integrity": "sha512-H3H46VXXhvHF/1EKIaAVpBXevX5Bmt1SYqpfQFIcb7JSajXwwncQv7gmhf5Q9H3jtwdY43YWq2TFoeLUkI7E4g==", + "license": "MIT", + "dependencies": { + "@chris.troutner/retry-queue": "1.0.11", + "@psf/bch-js": "7.1.1", + "bch-consumer": "1.6.2", + "bch-donation": "1.1.2", + "crypto-js": "4.0.0" + } + }, + "node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/@psf/bch-js": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@psf/bch-js/-/bch-js-7.1.1.tgz", + "integrity": "sha512-Wknq+q418Zb2Hmb6aDmxt/6VLrU81N+Iq9KP0KHGo1lFHeupJJonBkVt74R1ulry61F7MNRr39iDCTQsMM0ctg==", + "license": "MIT", + "dependencies": { + "@chris.troutner/bip32-utils": "1.0.5", + "@psf/bip21": "2.0.1", + "@psf/bitcoincashjs-lib": "4.0.3", + "@psf/coininfo": "4.0.0", + "axios": "1.13.2", + "bc-bip68": "1.0.5", + "bchaddrjs-slp": "0.2.14", + "bigi": "1.4.2", + "bignumber.js": "9.3.1", + "bip-schnorr": "0.6.7", + "bip38": "3.1.1", + "bip39": "3.1.0", + "bip66": "1.1.5", + "bitcoinjs-message": "2.2.0", + "bs58": "4.0.1", + "ecashaddrjs": "1.0.7", + "ini": "6.0.0", + "randombytes": "2.1.0", + "safe-buffer": "5.2.1", + "satoshi-bitcoin": "1.0.5", + "slp-mdm": "0.0.7", + "slp-parser": "0.0.4", + "wif": "2.0.6", + "x402-bch-axios": "1.1.1" + } + }, + "node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/minimal-slp-wallet/-/minimal-slp-wallet-6.1.0.tgz", + "integrity": "sha512-P24WzJu2kHg5aQtKkIVJ42Gu1dsYXWAc0z5MCmlinjDq11Bgy4ICBu73YxpNTB3TQwrWIIi2LJgsuQa/dqH50Q==", + "license": "MIT", + "dependencies": { + "@chris.troutner/retry-queue-commonjs": "1.0.8", + "@psf/bch-js": "6.8.1", + "apidoc": "0.51.0", + "bch-consumer": "1.6.2", + "bch-donation": "1.1.2", + "crypto-js": "4.0.0" + } + }, + "node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/@psf/bch-js": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@psf/bch-js/-/bch-js-6.8.1.tgz", + "integrity": "sha512-xu5YT9L3OhdILwJUmkV7Pg/WZ3r2aA6jD7fr5WMvo0OJSzzBBbXGShIbQNZp9+Vv9BoeVLCMqpZoW1Fp7OLCbg==", + "license": "MIT", + "dependencies": { + "@chris.troutner/bip32-utils": "1.0.5", + "@psf/bip21": "2.0.1", + "@psf/bitcoincash-ops": "2.0.0", + "@psf/bitcoincashjs-lib": "4.0.3", + "@psf/coininfo": "4.0.0", + "axios": "0.26.1", + "bc-bip68": "1.0.5", + "bchaddrjs-slp": "0.2.5", + "bigi": "1.4.2", + "bignumber.js": "9.0.0", + "bip-schnorr": "0.3.0", + "bip38": "2.0.2", + "bip39": "3.0.2", + "bip66": "1.1.5", + "bitcoinjs-message": "2.0.0", + "bs58": "4.0.1", + "ecashaddrjs": "1.0.7", + "ini": "1.3.8", + "randombytes": "2.0.6", + "safe-buffer": "5.1.2", + "satoshi-bitcoin": "1.0.4", + "slp-mdm": "0.0.6", + "slp-parser": "0.0.4", + "wif": "2.0.6" + } + }, + "node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.8" + } + }, + "node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/bchaddrjs-slp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/bchaddrjs-slp/-/bchaddrjs-slp-0.2.5.tgz", + "integrity": "sha512-33flmPcqMFswerKu7477DSUNMVMQR3tHDk3lvbmsdkEva+TxVGGWWE/p5Lqx9M/8t3vkbe7fzmVhj4QhChcCyA==", + "license": "MIT", + "dependencies": { + "bs58check": "^2.1.2", + "cashaddrjs-slp": "^0.2.11" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/bip-schnorr": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/bip-schnorr/-/bip-schnorr-0.3.0.tgz", + "integrity": "sha512-Sc1Hn2+1n+okPEW8G+JLjeaM12dsUOwr+oFlMDSKR9wYwNGMw0alskeBIHTmXxBxMZSWKhCW7PwKQVDyGmnaVg==", + "license": "MIT", + "dependencies": { + "ecurve": "^1.0.6", + "js-sha256": "^0.9.0", + "random-bytes": "^1.0.0", + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/bip38": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bip38/-/bip38-2.0.2.tgz", + "integrity": "sha512-22KDak0RDyghFbR0Si7wyq9IgY423YzGYzWLpGeofH3DaolOQqjD3mNN08eFoubKlbyclOQKFwtONMv2SD9V3A==", + "dependencies": { + "bigi": "^1.2.0", + "browserify-aes": "^1.0.1", + "bs58check": "<3.0.0", + "buffer-xor": "^1.0.2", + "create-hash": "^1.1.1", + "ecurve": "^1.0.0", + "scryptsy": "^2.0.0" + } + }, + "node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/bip39": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.2.tgz", + "integrity": "sha512-J4E1r2N0tUylTKt07ibXvhpT2c5pyAFgvuA5q1H9uDy6dEGpjV8jmymh3MTYJDLCNbIVClSB9FbND49I6N24MQ==", + "license": "ISC", + "dependencies": { + "@types/node": "11.11.6", + "create-hash": "^1.1.0", + "pbkdf2": "^3.0.9", + "randombytes": "^2.0.1" + } + }, + "node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/bitcoinjs-message": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bitcoinjs-message/-/bitcoinjs-message-2.0.0.tgz", + "integrity": "sha512-H5pJC7/eSqVjREiEOZ4jifX+7zXYP3Y28GIOIqg9hrgE7Vj8Eva9+HnVqnxwA1rJPOwZKuw0vo6k0UxgVc6q1A==", + "license": "MIT", + "dependencies": { + "bs58check": "^2.0.2", + "buffer-equals": "^1.0.3", + "create-hash": "^1.1.2", + "secp256k1": "^3.0.1", + "varuint-bitcoin": "^1.0.1" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/randombytes": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", + "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/satoshi-bitcoin": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/satoshi-bitcoin/-/satoshi-bitcoin-1.0.4.tgz", + "integrity": "sha512-YuHOmw5wsz6wuHIQdsz5b2cgtuKtV/jEcZ4NGmWN5tM/gz5T9Q+DSuLlnuf5BP/jsQWgR0ofmY4f8Dm6JnqSug==", + "license": "MIT", + "dependencies": { + "big.js": "^3.1.3" + } + }, + "node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/slp-mdm": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/slp-mdm/-/slp-mdm-0.0.6.tgz", + "integrity": "sha512-fbjlIg/o8OtzgK2JydC6POJp3Qup/rLgy4yB5hoLgxWRlERyJyE29ScwS3r9TTwPxe12qK55pyivAdNOZZXL0A==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/minimal-slp-wallet/node_modules/minimal-slp-wallet/node_modules/x402-bch-axios": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/x402-bch-axios/-/x402-bch-axios-1.1.1.tgz", + "integrity": "sha512-jQ0AyjAzvyTw7ejTTvUSIQiH8//wVn/SRwSgcJ3LeM38SJlJWpTMvvNpOpcjcZ+kFKOe4SsurOy/AcH7o+akjQ==", + "license": "MIT", + "dependencies": { + "@chris.troutner/retry-queue": "1.0.11", + "minimal-slp-wallet": "6.1.0" + } + }, + "node_modules/minimal-slp-wallet/node_modules/x402-bch-axios": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/x402-bch-axios/-/x402-bch-axios-2.1.0.tgz", + "integrity": "sha512-6IE6tUmg/+W4Poi6JBs5z0xjSOhvouzea9IH6BUnuBIdXDm0wv5cwT0cN5qAA4k7BE7hiB98LPiG6QIIkoGjVA==", + "license": "MIT", + "dependencies": { + "@chris.troutner/retry-queue": "1.0.11", + "minimal-slp-wallet": "7.0.2" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "license": "MIT" + }, "node_modules/minimatch": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", @@ -6114,7 +7514,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6274,6 +7673,28 @@ "node": ">=10" } }, + "node_modules/mock-property": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/mock-property/-/mock-property-1.1.2.tgz", + "integrity": "sha512-Rw0vY0wBdNtm3sbANVMDUgO5Htew9xotKoqcdK/a4rW8jf5KEl6MAWGMBhjKng6gpPjX8SmO8f+65ssnxNroTw==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "isarray": "^2.0.5", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -6289,6 +7710,12 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/nan": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz", + "integrity": "sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", @@ -6328,7 +7755,6 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, "license": "MIT" }, "node_modules/nise": { @@ -6362,11 +7788,16 @@ "isarray": "0.0.1" } }, + "node_modules/node-addon-api": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", + "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==", + "license": "MIT" + }, "node_modules/node-exports-info": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", - "dev": true, "license": "MIT", "dependencies": { "array.prototype.flatmap": "^1.3.3", @@ -6385,7 +7816,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -6406,7 +7836,6 @@ "version": "2.0.47", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -6416,7 +7845,6 @@ "version": "2.0.22", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==", - "dev": true, "license": "MIT", "dependencies": { "chokidar": "^3.5.2", @@ -6445,7 +7873,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -6456,7 +7883,6 @@ "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.1" @@ -6466,7 +7892,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -6476,7 +7901,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -6489,14 +7913,12 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nodemon/node_modules/semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver" @@ -6506,7 +7928,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^3.0.0" @@ -6519,7 +7940,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6568,11 +7988,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6582,7 +8017,6 @@ "version": "4.1.7", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -6603,7 +8037,6 @@ "version": "1.1.9", "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -6684,7 +8117,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -6741,7 +8173,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.6", @@ -6787,11 +8218,54 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-7.3.0.tgz", + "integrity": "sha512-5fP+yVQ0qp0rEfZoDTlP2c3RYBgxvRsw30qO+VtPPc95lyvSG+x6USSh1TuLB4n96IO6I8/oXQGsTgtna4q2nQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.7", + "p-timeout": "^5.0.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-5.1.1.tgz", + "integrity": "sha512-i69WkEU5ZAL8mrmdmVviWwU+DN+IUF8f4sSJThoJ3z5A7Nn5iuO5ROX3Boye0u+uYQLOSfgFl7SuFZCjlAVbQA==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.1", + "retry": "^0.13.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-5.1.0.tgz", + "integrity": "sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -6850,7 +8324,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6878,7 +8351,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -6914,11 +8386,27 @@ "node": "*" } }, + "node_modules/pbkdf2": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.6.tgz", + "integrity": "sha512-BT6eelPB1EyGHo8pC0o9Bl6k6SYVhKO1jEbd3lcTrtr7XHdjP8BW1YpfCV3G9Kwkxgattk+S5q2/RvuttCsS1g==", + "license": "MIT", + "dependencies": { + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "ripemd160": "^2.0.3", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.12", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -7027,7 +8515,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, "license": "MIT", "dependencies": { "find-up": "^4.0.0" @@ -7040,7 +8527,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -7054,7 +8540,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -7067,7 +8552,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -7083,7 +8567,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -7096,7 +8579,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7116,12 +8598,17 @@ "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -7134,11 +8621,16 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true, "license": "MIT" }, "node_modules/punycode": { @@ -7166,6 +8658,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -7186,11 +8684,19 @@ ], "license": "MIT" }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" @@ -7265,7 +8771,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -7278,7 +8783,6 @@ "version": "0.7.1", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", - "dev": true, "license": "MIT", "dependencies": { "resolve": "^1.9.0" @@ -7291,7 +8795,6 @@ "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7313,7 +8816,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -7336,7 +8838,6 @@ "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -7380,17 +8881,21 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, "node_modules/resolve": { "version": "2.0.0-next.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7414,7 +8919,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, "license": "MIT", "dependencies": { "resolve-from": "^5.0.0" @@ -7427,7 +8931,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7492,6 +8995,15 @@ "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", "license": "ISC" }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -7519,6 +9031,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/ripemd160": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", + "license": "MIT", + "dependencies": { + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -7569,7 +9094,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.9", @@ -7609,7 +9133,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7654,11 +9177,28 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/satoshi-bitcoin": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/satoshi-bitcoin/-/satoshi-bitcoin-1.0.5.tgz", + "integrity": "sha512-iCUOSe8yTOqetYcj/n4ziv0psqRMRZT4+YhcMrQIYpjnx3Pgn9uiTlaUItNMMB/0sldINEGkJvxwTW55OfrYPg==", + "license": "MIT", + "dependencies": { + "big.js": "^3.1.3" + } + }, + "node_modules/satoshi-bitcoin/node_modules/big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/schema-utils": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", @@ -7678,7 +9218,6 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -7695,7 +9234,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3" @@ -7708,14 +9246,38 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, "license": "MIT" }, + "node_modules/scryptsy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scryptsy/-/scryptsy-2.1.0.tgz", + "integrity": "sha512-1CdSqHQowJBnMAFyPEBRfqag/YP9OF394FV+4YREIJX4ljD7OxvQRDayyoyyCk+senRjSkP6VnUNQmVQqB6g7w==", + "license": "MIT" + }, + "node_modules/secp256k1": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.8.1.tgz", + "integrity": "sha512-tArjQw2P0RTdY7QmkNehgp6TVvQXq6ulIhxv8gaH6YubKG/wxxAoNKcbuXjDhybbc+b2Ihc7e0xxiGN744UIiQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "bip66": "^1.1.5", + "bn.js": "^4.11.8", + "create-hash": "^1.2.0", + "drbg.js": "^1.0.1", + "elliptic": "^6.5.7", + "nan": "^2.14.0", + "safe-buffer": "^5.1.2" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/semver": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -7738,7 +9300,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -7756,7 +9317,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -7772,7 +9332,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -7789,11 +9348,30 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, "license": "MIT", "dependencies": { "kind-of": "^6.0.2" @@ -7925,7 +9503,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", - "dev": true, "license": "MIT", "dependencies": { "semver": "~7.0.0" @@ -7938,7 +9515,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -7974,18 +9550,34 @@ "node": ">=0.3.1" } }, + "node_modules/slp-mdm": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/slp-mdm/-/slp-mdm-0.0.7.tgz", + "integrity": "sha512-XlnDS7y8fnEt9I5lw/GqcrDjTGw5vZon83xlliLyihz2EvjE135ubIBzq4bmjVOrM669G9A6bkqEp5Y1trPV3A==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/slp-parser": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slp-parser/-/slp-parser-0.0.4.tgz", + "integrity": "sha512-AvbslJumkzGfMGWNvuE2pWx2nyHEk/VgQ7l119kDKIFRTuRUWOkyOULLauw5laGRQsBRThg6NCx/TsR3grX6GA==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", - "dev": true, "license": "MIT" }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -7995,7 +9587,6 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -8091,7 +9682,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -8181,19 +9771,19 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -8206,7 +9796,6 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -8225,7 +9814,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -8302,7 +9890,6 @@ "version": "3.3.4", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 12.13.0" @@ -8332,7 +9919,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8345,7 +9931,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -8355,11 +9940,124 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tape": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/tape/-/tape-5.10.2.tgz", + "integrity": "sha512-MfhVVAWa834kfYL4KgrqHblWfwkjaQvfaQhAX/bBTNQsK8oJ4ztpUe7jiemR2Vegw4pYwpnQMy51MhEmRzbbsg==", + "license": "MIT", + "dependencies": { + "@ljharb/now": "^1.0.1", + "@ljharb/resumer": "^0.1.3", + "@ljharb/through": "^2.3.14", + "array.prototype.every": "^1.1.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "deep-equal": "^2.2.3", + "defined": "^1.0.1", + "dotignore": "^0.1.2", + "es-object-atoms": "^1.1.2", + "for-each": "^0.3.5", + "get-package-type": "^0.1.0", + "glob": "^7.2.3", + "has-dynamic-import": "^2.1.1", + "hasown": "^2.0.4", + "inherits": "^2.0.4", + "is-regex": "^1.2.1", + "minimist": "^1.2.8", + "mock-property": "^1.1.0", + "object-inspect": "^1.13.4", + "object-is": "^1.1.6", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "resolve": "^2.0.0-next.7", + "string.prototype.trim": "^1.2.11" + }, + "bin": { + "tape": "bin/tape" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tape/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/tape/node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tape/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tape/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/terser": { "version": "5.48.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -8378,7 +10076,6 @@ "version": "5.6.1", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", @@ -8439,7 +10136,6 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, "license": "MIT" }, "node_modules/test-exclude": { @@ -8547,6 +10243,20 @@ "dev": true, "license": "MIT" }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -8572,7 +10282,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", - "dev": true, "license": "ISC", "bin": { "nodetouch": "bin/nodetouch.js" @@ -8662,7 +10371,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -8677,7 +10385,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -8697,7 +10404,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -8719,7 +10425,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.9", @@ -8736,18 +10441,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typeforce": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", + "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==", + "license": "MIT" + }, "node_modules/uc.micro": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", - "dev": true, "license": "MIT" }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, "license": "BSD-2-Clause", "optional": true, "bin": { @@ -8761,7 +10470,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -8780,21 +10488,18 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true, "license": "MIT" }, "node_modules/undici-types": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", - "dev": true, "license": "MIT" }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" @@ -8813,7 +10518,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, "funding": [ { "type": "opencollective", @@ -8850,6 +10554,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -8871,6 +10585,15 @@ "node": ">=10.12.0" } }, + "node_modules/varuint-bitcoin": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz", + "integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.1" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -8884,7 +10607,6 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", - "dev": true, "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", @@ -8898,7 +10620,6 @@ "version": "5.107.2", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.2.tgz", "integrity": "sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.8", @@ -8945,7 +10666,6 @@ "version": "4.10.0", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", - "dev": true, "license": "MIT", "dependencies": { "@discoveryjs/json-ext": "^0.5.0", @@ -8993,7 +10713,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 10" @@ -9003,7 +10722,6 @@ "version": "5.10.0", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "dev": true, "license": "MIT", "dependencies": { "clone-deep": "^4.0.1", @@ -9018,7 +10736,6 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dev": true, "license": "MIT", "dependencies": { "source-list-map": "^2.0.0", @@ -9029,7 +10746,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", @@ -9043,7 +10759,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -9053,7 +10768,6 @@ "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -9063,7 +10777,6 @@ "version": "3.5.0", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz", "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.13.0" @@ -9088,7 +10801,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, "license": "MIT", "dependencies": { "is-bigint": "^1.1.0", @@ -9108,7 +10820,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -9136,7 +10847,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, "license": "MIT", "dependencies": { "is-map": "^2.0.3", @@ -9155,7 +10865,6 @@ "version": "1.1.21", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.21.tgz", "integrity": "sha512-zbRA8cVm6io/d5W8uIe2hblzN76/Wm3v/yiythQvr+dpBWeqhPSWIDNj4zOyHi4zKbMK6DN34Xsr9jPHJERAEw==", - "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -9173,11 +10882,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wif": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", + "integrity": "sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ==", + "license": "MIT", + "dependencies": { + "bs58check": "<3.0.0" + } + }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", - "dev": true, "license": "MIT" }, "node_modules/winston": { @@ -9246,7 +10963,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, "license": "MIT" }, "node_modules/workerpool": { @@ -9369,9 +11085,18 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, + "node_modules/x402-bch-axios": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/x402-bch-axios/-/x402-bch-axios-2.2.1.tgz", + "integrity": "sha512-N23DpQRA746bqGDTCJYVQ8QL55PoxE6LTSNMFzx5sySsvnBem6H0hQLSf2N9e45O8xPVw3ulSvXncgTcVM8ikw==", + "license": "MIT", + "dependencies": { + "@chris.troutner/retry-queue": "1.0.11", + "minimal-slp-wallet": "7.0.5" + } + }, "node_modules/xdg-basedir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", diff --git a/psf-memo-db/package.json b/psf-memo-db/package.json index 45c40d3..3c24540 100644 --- a/psf-memo-db/package.json +++ b/psf-memo-db/package.json @@ -20,6 +20,7 @@ "url": "localhost:5021" }, "dependencies": { + "@psf/bch-js": "7.1.11", "dotenv": "17.2.3", "kcors": "2.2.2", "koa": "2.13.1", diff --git a/psf-memo-db/src/adapters/follow-query.js b/psf-memo-db/src/adapters/follow-query.js new file mode 100644 index 0000000..cf5c410 --- /dev/null +++ b/psf-memo-db/src/adapters/follow-query.js @@ -0,0 +1,84 @@ +/* + Adapter for querying Memo follow relationships from the follows LevelDB. + + The indexer stores a follow record keyed as `${followerAddr}:${followeeHash160}` + with an `unfollow` flag. This adapter exposes: + - isFollowing(followerAddr, followeeAddr) + - listFollowing(followerAddr) + - listFollowers(followeeAddr) + + Cash addresses are converted to 20-byte hash160 hex via bch-js Address.toHash160() + and back via Address.hash160ToCash(). +*/ + +import BCHJS from '@psf/bch-js' + +class FollowQuery { + constructor (localConfig = {}) { + const { followsDb, bchjs = new BCHJS({ restURL: process.env.RESTURL || 'https://api.fullstack.cash/v5/' }) } = localConfig + if (!followsDb) { + throw new Error('followsDb required when instantiating FollowQuery adapter.') + } + this.followsDb = followsDb + this.bchjs = bchjs + this.isFollowing = this.isFollowing.bind(this) + this.listFollowing = this.listFollowing.bind(this) + this.listFollowers = this.listFollowers.bind(this) + } + + // Return true when followerAddr has an active (not unfollowed) follow record + // for followeeAddr. + async isFollowing (followerAddr, followeeAddr) { + const hash160 = this._toHash160(followeeAddr) + const key = `${followerAddr}:${hash160}` + try { + const record = await this.followsDb.get(key) + return record.unfollow !== true + } catch (err) { + if (err.notFound) return false + throw err + } + } + + // Return the cash addresses the follower currently follows. + async listFollowing (followerAddr) { + const prefix = `${followerAddr}:` + const following = new Set() + for await (const [key, record] of this.followsDb.iterator({ gte: prefix, lt: this._nextString(prefix) })) { + if (record.unfollow === true) continue + const hash160 = key.slice(prefix.length) + following.add(this._toCashAddress(hash160)) + } + return Array.from(following) + } + + // Return the cash addresses that currently follow the followee. + async listFollowers (followeeAddr) { + const hash160 = this._toHash160(followeeAddr) + const suffix = `:${hash160}` + const followers = new Set() + for await (const [key, record] of this.followsDb.iterator()) { + if (!key.endsWith(suffix)) continue + if (record.unfollow === true) continue + const followerAddr = key.slice(0, key.length - suffix.length) + followers.add(followerAddr) + } + return Array.from(followers) + } + + _toHash160 (addr) { + return this.bchjs.Address.toHash160(addr) + } + + _toCashAddress (hash160) { + return this.bchjs.Address.hash160ToCash(hash160) + } + + // Lexicographic successor for a string, used as an exclusive upper bound + // for LevelDB prefix scans. + _nextString (s) { + return s.slice(0, -1) + String.fromCharCode(s.charCodeAt(s.length - 1) + 1) + } +} + +export default FollowQuery diff --git a/psf-memo-db/src/adapters/index.js b/psf-memo-db/src/adapters/index.js index 690770f..466a311 100644 --- a/psf-memo-db/src/adapters/index.js +++ b/psf-memo-db/src/adapters/index.js @@ -6,6 +6,7 @@ import LevelDb from './level-db.js' import DbBackup from './db-backup.js' import ProfileQuery from './profile-query.js' import PostQuery from './post-query.js' +import FollowQuery from './follow-query.js' class Adapters { constructor () { @@ -29,6 +30,9 @@ class Adapters { postChildrenDb: level.postChildrenDb, likesDb: level.likesDb }) + this.followQuery = new FollowQuery({ + followsDb: level.followsDb + }) return true } diff --git a/psf-memo-db/src/controllers/rest-api/follow/controller.js b/psf-memo-db/src/controllers/rest-api/follow/controller.js new file mode 100644 index 0000000..8e04ce7 --- /dev/null +++ b/psf-memo-db/src/controllers/rest-api/follow/controller.js @@ -0,0 +1,111 @@ +/* + REST API controller for /follow routes. +*/ + +import wlogger from '../../../adapters/wlogger.js' + +class FollowRESTControllerLib { + constructor (localConfig = {}) { + this.adapters = localConfig.adapters + this.useCases = localConfig.useCases + if (!this.adapters) { + throw new Error('Adapters required for Follow REST Controller.') + } + if (!this.useCases) { + throw new Error('Use Cases required for Follow REST Controller.') + } + + this.getFollowState = this.getFollowState.bind(this) + this.getFollowing = this.getFollowing.bind(this) + this.getFollowers = this.getFollowers.bind(this) + this.handleError = this.handleError.bind(this) + } + + handleError (ctx, err) { + if (err.status) { + ctx.throw(err.status, err.message || err) + } else { + wlogger.error('Error in follow controller: ', err) + ctx.throw(500, err.message || 'Internal server error') + } + } + + /** + * @api {get} /follow/state Check follow state + * @apiPermission public + * @apiName GetFollowState + * @apiGroup REST Follow + * + * @apiDescription Returns whether a follower address follows a followee address. + * + * @apiQuery {String} follower Cash address of the follower + * @apiQuery {String} followee Cash address of the followee + * + * @apiExample Example usage: + * curl -X GET "localhost:5021/follow/state?follower=bitcoincash:q...&followee=bitcoincash:q..." + * + * @apiSuccess {String} followerAddr Follower cash address + * @apiSuccess {String} followeeAddr Followee cash address + * @apiSuccess {Boolean} following True when an active follow record exists + */ + async getFollowState (ctx) { + try { + const { follower, followee } = ctx.query + ctx.body = await this.useCases.followState.execute({ followerAddr: follower, followeeAddr: followee }) + } catch (err) { + this.handleError(ctx, err) + } + } + + /** + * @api {get} /follow/following List following + * @apiPermission public + * @apiName GetFollowing + * @apiGroup REST Follow + * + * @apiDescription Returns the addresses a follower currently follows. + * + * @apiParam {String} follower Cash address of the follower + * + * @apiExample Example usage: + * curl -X GET "localhost:5021/follow/following/bitcoincash:q..." + * + * @apiSuccess {String} followerAddr Follower cash address + * @apiSuccess {String[]} following Array of followee cash addresses + */ + async getFollowing (ctx) { + try { + const { follower } = ctx.params + ctx.body = await this.useCases.listFollowing.execute({ followerAddr: follower }) + } catch (err) { + this.handleError(ctx, err) + } + } + + /** + * @api {get} /follow/followers List followers + * @apiPermission public + * @apiName GetFollowers + * @apiGroup REST Follow + * + * @apiDescription Returns the addresses that currently follow a followee. + * + * @apiParam {String} followee Cash address of the followee + * + * @apiExample Example usage: + * curl -X GET "localhost:5021/follow/followers/bitcoincash:q..." + * + * @apiSuccess {String} followeeAddr Followee cash address + * @apiSuccess {String[]} followers Array of follower cash addresses + */ + async getFollowers (ctx) { + try { + const { followee } = ctx.params + ctx.body = await this.useCases.listFollowers.execute({ followeeAddr: followee }) + } catch (err) { + this.handleError(ctx, err) + } + } +} + +export default FollowRESTControllerLib diff --git a/psf-memo-db/src/controllers/rest-api/follow/index.js b/psf-memo-db/src/controllers/rest-api/follow/index.js new file mode 100644 index 0000000..3181549 --- /dev/null +++ b/psf-memo-db/src/controllers/rest-api/follow/index.js @@ -0,0 +1,36 @@ +/* + REST API router for /follow routes. +*/ + +import Router from 'koa-router' +import FollowRESTControllerLib from './controller.js' + +class FollowRouter { + constructor (localConfig = {}) { + this.adapters = localConfig.adapters + this.useCases = localConfig.useCases + if (!this.adapters) { + throw new Error('Adapters required when instantiating Follow REST Controller.') + } + if (!this.useCases) { + throw new Error('Use Cases required when instantiating Follow REST Controller.') + } + + this.followRESTController = new FollowRESTControllerLib({ + adapters: this.adapters, + useCases: this.useCases + }) + this.router = new Router({ prefix: '/follow' }) + } + + attach (app) { + this.router.get('/state', this.followRESTController.getFollowState) + this.router.get('/following/:follower', this.followRESTController.getFollowing) + this.router.get('/followers/:followee', this.followRESTController.getFollowers) + + app.use(this.router.routes()) + app.use(this.router.allowedMethods()) + } +} + +export default FollowRouter diff --git a/psf-memo-db/src/controllers/rest-api/index.js b/psf-memo-db/src/controllers/rest-api/index.js index 92796e7..7e59853 100644 --- a/psf-memo-db/src/controllers/rest-api/index.js +++ b/psf-memo-db/src/controllers/rest-api/index.js @@ -6,6 +6,7 @@ import LevelRESTController from './level/index.js' import HealthRouter from './health/index.js' import ProfileRouter from './profile/index.js' import PostsRouter from './posts/index.js' +import FollowRouter from './follow/index.js' class RESTControllers { constructor (localConfig = {}) { @@ -31,6 +32,9 @@ class RESTControllers { const postsRouter = new PostsRouter(dependencies) postsRouter.attach(app) + + const followRouter = new FollowRouter(dependencies) + followRouter.attach(app) } } diff --git a/psf-memo-db/src/use-cases/follow-state.js b/psf-memo-db/src/use-cases/follow-state.js new file mode 100644 index 0000000..0ad59f4 --- /dev/null +++ b/psf-memo-db/src/use-cases/follow-state.js @@ -0,0 +1,34 @@ +/* + Use case: report whether one address follows another. + + Returns { followerAddr, followeeAddr, following: boolean }. +*/ + +class FollowState { + constructor (localConfig = {}) { + this.adapters = localConfig.adapters + if (!this.adapters) { + throw new Error('Adapters required when instantiating FollowState use case.') + } + if (!this.adapters.followQuery) { + throw new Error('followQuery adapter required for FollowState use case.') + } + this.execute = this.execute.bind(this) + } + + async execute (inObj = {}) { + const { followerAddr, followeeAddr } = inObj + if (!followerAddr || typeof followerAddr !== 'string') { + throw new Error('followerAddr is required') + } + if (!followeeAddr || typeof followeeAddr !== 'string') { + throw new Error('followeeAddr is required') + } + + const following = await this.adapters.followQuery.isFollowing(followerAddr, followeeAddr) + + return { followerAddr, followeeAddr, following } + } +} + +export default FollowState diff --git a/psf-memo-db/src/use-cases/index.js b/psf-memo-db/src/use-cases/index.js index 3003f8f..cab7027 100644 --- a/psf-memo-db/src/use-cases/index.js +++ b/psf-memo-db/src/use-cases/index.js @@ -6,6 +6,9 @@ import ListRecentProfiles from './list-recent-profiles.js' import ListRecentPosts from './list-recent-posts.js' import ListPostsByAddr from './list-posts-by-addr.js' import GetPostThread from './get-post-thread.js' +import FollowState from './follow-state.js' +import ListFollowing from './list-following.js' +import ListFollowers from './list-followers.js' class UseCases { constructor (localConfig = {}) { @@ -21,6 +24,9 @@ class UseCases { this.listRecentPosts = null this.listPostsByAddr = null this.getPostThread = null + this.followState = null + this.listFollowing = null + this.listFollowers = null } async start () { @@ -40,6 +46,18 @@ class UseCases { adapters: this.adapters }) + this.followState = new FollowState({ + adapters: this.adapters + }) + + this.listFollowing = new ListFollowing({ + adapters: this.adapters + }) + + this.listFollowers = new ListFollowers({ + adapters: this.adapters + }) + console.log('Use cases initialized.') return true diff --git a/psf-memo-db/src/use-cases/list-followers.js b/psf-memo-db/src/use-cases/list-followers.js new file mode 100644 index 0000000..312ee57 --- /dev/null +++ b/psf-memo-db/src/use-cases/list-followers.js @@ -0,0 +1,31 @@ +/* + Use case: list the addresses that currently follow a followee. + + Returns { followeeAddr, followers: string[] }. +*/ + +class ListFollowers { + constructor (localConfig = {}) { + this.adapters = localConfig.adapters + if (!this.adapters) { + throw new Error('Adapters required when instantiating ListFollowers use case.') + } + if (!this.adapters.followQuery) { + throw new Error('followQuery adapter required for ListFollowers use case.') + } + this.execute = this.execute.bind(this) + } + + async execute (inObj = {}) { + const { followeeAddr } = inObj + if (!followeeAddr || typeof followeeAddr !== 'string') { + throw new Error('followeeAddr is required') + } + + const followers = await this.adapters.followQuery.listFollowers(followeeAddr) + + return { followeeAddr, followers } + } +} + +export default ListFollowers diff --git a/psf-memo-db/src/use-cases/list-following.js b/psf-memo-db/src/use-cases/list-following.js new file mode 100644 index 0000000..1efe85f --- /dev/null +++ b/psf-memo-db/src/use-cases/list-following.js @@ -0,0 +1,31 @@ +/* + Use case: list the addresses a follower currently follows. + + Returns { followerAddr, following: string[] }. +*/ + +class ListFollowing { + constructor (localConfig = {}) { + this.adapters = localConfig.adapters + if (!this.adapters) { + throw new Error('Adapters required when instantiating ListFollowing use case.') + } + if (!this.adapters.followQuery) { + throw new Error('followQuery adapter required for ListFollowing use case.') + } + this.execute = this.execute.bind(this) + } + + async execute (inObj = {}) { + const { followerAddr } = inObj + if (!followerAddr || typeof followerAddr !== 'string') { + throw new Error('followerAddr is required') + } + + const following = await this.adapters.followQuery.listFollowing(followerAddr) + + return { followerAddr, following } + } +} + +export default ListFollowing diff --git a/psf-memo-db/test/unit/adapters/follow-query.unit.js b/psf-memo-db/test/unit/adapters/follow-query.unit.js new file mode 100644 index 0000000..10b8d06 --- /dev/null +++ b/psf-memo-db/test/unit/adapters/follow-query.unit.js @@ -0,0 +1,112 @@ +/* + Unit tests for the FollowQuery adapter. +*/ + +import { assert } from 'chai' +import FollowQuery from '../../../src/adapters/follow-query.js' + +function makeFollowsDb (records = {}) { + const store = new Map(Object.entries(records)) + return { + async get (key) { + if (!store.has(key)) { + const err = new Error('not found') + err.notFound = true + throw err + } + return store.get(key) + }, + iterator (opts = {}) { + const entries = Array.from(store.entries()).sort((a, b) => a[0].localeCompare(b[0])) + const { gte, lt } = opts + const filtered = entries.filter(([key]) => { + if (gte && key < gte) return false + if (lt && key >= lt) return false + return true + }) + let i = 0 + return { + [Symbol.asyncIterator] () { + return this + }, + async next () { + if (i >= filtered.length) return { value: undefined, done: true } + const entry = filtered[i++] + return { value: entry, done: false } + }, + async close () {} + } + } + } +} + +const FOLLOWER = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' +const FOLLOWEE = 'bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy' +const OTHER_FOLLOWER = 'bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a' + +describe('#FollowQuery', () => { + it('should throw when followsDb is missing', () => { + assert.throws(() => new FollowQuery({}), /followsDb required/) + }) + + it('isFollowing returns false when no follow record exists', async () => { + const query = new FollowQuery({ followsDb: makeFollowsDb({}) }) + const result = await query.isFollowing(FOLLOWER, FOLLOWEE) + assert.equal(result, false) + }) + + it('isFollowing returns true for an active follow record', async () => { + const hash160 = 'cb481232299cd5743151ac4b2d63ae198e7bb0a9' + const followsDb = makeFollowsDb({ + [`${FOLLOWER}:${hash160}`]: { followerAddr: FOLLOWER, followeePkHash: hash160, unfollow: false } + }) + const query = new FollowQuery({ followsDb }) + const result = await query.isFollowing(FOLLOWER, FOLLOWEE) + assert.equal(result, true) + }) + + it('isFollowing returns false when the latest record is an unfollow', async () => { + const hash160 = 'cb481232299cd5743151ac4b2d63ae198e7bb0a9' + const followsDb = makeFollowsDb({ + [`${FOLLOWER}:${hash160}`]: { followerAddr: FOLLOWER, followeePkHash: hash160, unfollow: true } + }) + const query = new FollowQuery({ followsDb }) + const result = await query.isFollowing(FOLLOWER, FOLLOWEE) + assert.equal(result, false) + }) + + it('listFollowing returns active followees for a follower', async () => { + const hash160 = 'cb481232299cd5743151ac4b2d63ae198e7bb0a9' + const hash160Two = '44c44cfcb6e4e00386c7b0d14eaac6b7f47695e3' + const followsDb = makeFollowsDb({ + [`${FOLLOWER}:${hash160}`]: { followerAddr: FOLLOWER, followeePkHash: hash160, unfollow: false }, + [`${FOLLOWER}:${hash160Two}`]: { followerAddr: FOLLOWER, followeePkHash: hash160Two, unfollow: true }, + [`${OTHER_FOLLOWER}:${hash160}`]: { followerAddr: OTHER_FOLLOWER, followeePkHash: hash160, unfollow: false } + }) + const query = new FollowQuery({ followsDb }) + const result = await query.listFollowing(FOLLOWER) + assert.deepEqual(result, [FOLLOWEE]) + }) + + it('listFollowers returns active followers for a followee', async () => { + const hash160 = 'cb481232299cd5743151ac4b2d63ae198e7bb0a9' + const followsDb = makeFollowsDb({ + [`${FOLLOWER}:${hash160}`]: { followerAddr: FOLLOWER, followeePkHash: hash160, unfollow: false }, + [`${OTHER_FOLLOWER}:${hash160}`]: { followerAddr: OTHER_FOLLOWER, followeePkHash: hash160, unfollow: false }, + [`${FOLLOWER}:44c44cfcb6e4e00386c7b0d14eaac6b7f47695e3`]: { followerAddr: FOLLOWER, followeePkHash: '44c44cfcb6e4e00386c7b0d14eaac6b7f47695e3', unfollow: false } + }) + const query = new FollowQuery({ followsDb }) + const result = await query.listFollowers(FOLLOWEE) + assert.sameMembers(result, [FOLLOWER, OTHER_FOLLOWER]) + }) + + it('listFollowers ignores unfollow records', async () => { + const hash160 = 'cb481232299cd5743151ac4b2d63ae198e7bb0a9' + const followsDb = makeFollowsDb({ + [`${FOLLOWER}:${hash160}`]: { followerAddr: FOLLOWER, followeePkHash: hash160, unfollow: true } + }) + const query = new FollowQuery({ followsDb }) + const result = await query.listFollowers(FOLLOWEE) + assert.deepEqual(result, []) + }) +}) diff --git a/psf-memo-db/test/unit/controllers/follow.controller.unit.js b/psf-memo-db/test/unit/controllers/follow.controller.unit.js new file mode 100644 index 0000000..6348e15 --- /dev/null +++ b/psf-memo-db/test/unit/controllers/follow.controller.unit.js @@ -0,0 +1,75 @@ +/* + Unit tests for the Follow REST controller. +*/ + +import { assert } from 'chai' +import sinon from 'sinon' +import FollowRESTControllerLib from '../../../src/controllers/rest-api/follow/controller.js' + +describe('#FollowRESTController', () => { + let sandbox + + beforeEach(() => { + sandbox = sinon.createSandbox() + }) + + afterEach(() => sandbox.restore()) + + function makeUut (useCases) { + return new FollowRESTControllerLib({ + adapters: {}, + useCases + }) + } + + function makeCtx (query = {}, params = {}) { + return { query, params, body: null, throw: sandbox.stub() } + } + + it('should return follow state from use case', async () => { + const followState = { execute: sandbox.stub().resolves({ followerAddr: 'a', followeeAddr: 'b', following: true }) } + const uut = makeUut({ followState }) + const ctx = makeCtx({ follower: 'a', followee: 'b' }) + + await uut.getFollowState(ctx) + + assert.equal(followState.execute.callCount, 1) + assert.deepEqual(followState.execute.firstCall.args[0], { followerAddr: 'a', followeeAddr: 'b' }) + assert.equal(ctx.body.following, true) + }) + + it('should return following list from use case', async () => { + const listFollowing = { execute: sandbox.stub().resolves({ followerAddr: 'a', following: ['b', 'c'] }) } + const uut = makeUut({ listFollowing }) + const ctx = makeCtx({}, { follower: 'a' }) + + await uut.getFollowing(ctx) + + assert.equal(listFollowing.execute.callCount, 1) + assert.deepEqual(listFollowing.execute.firstCall.args[0], { followerAddr: 'a' }) + assert.deepEqual(ctx.body.following, ['b', 'c']) + }) + + it('should return followers list from use case', async () => { + const listFollowers = { execute: sandbox.stub().resolves({ followeeAddr: 'b', followers: ['a', 'c'] }) } + const uut = makeUut({ listFollowers }) + const ctx = makeCtx({}, { followee: 'b' }) + + await uut.getFollowers(ctx) + + assert.equal(listFollowers.execute.callCount, 1) + assert.deepEqual(listFollowers.execute.firstCall.args[0], { followeeAddr: 'b' }) + assert.deepEqual(ctx.body.followers, ['a', 'c']) + }) + + it('should handle use case errors', async () => { + const followState = { execute: sandbox.stub().rejects(new Error('boom')) } + const uut = makeUut({ followState }) + const ctx = makeCtx({ follower: 'a', followee: 'b' }) + + await uut.getFollowState(ctx) + + assert.equal(ctx.throw.callCount, 1) + assert.equal(ctx.throw.firstCall.args[0], 500) + }) +}) diff --git a/psf-memo-db/test/unit/use-cases/follow-state.unit.js b/psf-memo-db/test/unit/use-cases/follow-state.unit.js new file mode 100644 index 0000000..11e8c89 --- /dev/null +++ b/psf-memo-db/test/unit/use-cases/follow-state.unit.js @@ -0,0 +1,72 @@ +/* + Unit tests for the FollowState use case. +*/ + +import { assert } from 'chai' +import FollowState from '../../../src/use-cases/follow-state.js' + +function makeAdapters (isFollowingResult) { + return { + followQuery: { + async isFollowing (followerAddr, followeeAddr) { + return isFollowingResult + } + } + } +} + +describe('#FollowState', () => { + it('should throw when adapters is missing', () => { + assert.throws(() => new FollowState({}), /Adapters required/) + }) + + it('should throw when followQuery adapter is missing', () => { + assert.throws(() => new FollowState({ adapters: {} }), /followQuery adapter required/) + }) + + it('should return following=true', async () => { + const useCase = new FollowState({ adapters: makeAdapters(true) }) + const result = await useCase.execute({ + followerAddr: 'bitcoincash:follower', + followeeAddr: 'bitcoincash:followee' + }) + assert.deepEqual(result, { + followerAddr: 'bitcoincash:follower', + followeeAddr: 'bitcoincash:followee', + following: true + }) + }) + + it('should return following=false', async () => { + const useCase = new FollowState({ adapters: makeAdapters(false) }) + const result = await useCase.execute({ + followerAddr: 'bitcoincash:follower', + followeeAddr: 'bitcoincash:followee' + }) + assert.deepEqual(result, { + followerAddr: 'bitcoincash:follower', + followeeAddr: 'bitcoincash:followee', + following: false + }) + }) + + it('should reject a missing followerAddr', async () => { + const useCase = new FollowState({ adapters: makeAdapters(false) }) + try { + await useCase.execute({ followeeAddr: 'bitcoincash:followee' }) + assert.fail('expected error') + } catch (err) { + assert.match(err.message, /followerAddr is required/) + } + }) + + it('should reject a missing followeeAddr', async () => { + const useCase = new FollowState({ adapters: makeAdapters(false) }) + try { + await useCase.execute({ followerAddr: 'bitcoincash:follower' }) + assert.fail('expected error') + } catch (err) { + assert.match(err.message, /followeeAddr is required/) + } + }) +}) diff --git a/psf-memo-db/test/unit/use-cases/list-followers.unit.js b/psf-memo-db/test/unit/use-cases/list-followers.unit.js new file mode 100644 index 0000000..14ee706 --- /dev/null +++ b/psf-memo-db/test/unit/use-cases/list-followers.unit.js @@ -0,0 +1,45 @@ +/* + Unit tests for the ListFollowers use case. +*/ + +import { assert } from 'chai' +import ListFollowers from '../../../src/use-cases/list-followers.js' + +function makeAdapters (followers) { + return { + followQuery: { + async listFollowers (followeeAddr) { + return followers + } + } + } +} + +describe('#ListFollowers', () => { + it('should throw when adapters is missing', () => { + assert.throws(() => new ListFollowers({}), /Adapters required/) + }) + + it('should throw when followQuery adapter is missing', () => { + assert.throws(() => new ListFollowers({ adapters: {} }), /followQuery adapter required/) + }) + + it('should return the followers list', async () => { + const useCase = new ListFollowers({ adapters: makeAdapters(['bitcoincash:a', 'bitcoincash:b']) }) + const result = await useCase.execute({ followeeAddr: 'bitcoincash:followee' }) + assert.deepEqual(result, { + followeeAddr: 'bitcoincash:followee', + followers: ['bitcoincash:a', 'bitcoincash:b'] + }) + }) + + it('should reject a missing followeeAddr', async () => { + const useCase = new ListFollowers({ adapters: makeAdapters([]) }) + try { + await useCase.execute({}) + assert.fail('expected error') + } catch (err) { + assert.match(err.message, /followeeAddr is required/) + } + }) +}) diff --git a/psf-memo-db/test/unit/use-cases/list-following.unit.js b/psf-memo-db/test/unit/use-cases/list-following.unit.js new file mode 100644 index 0000000..8f4dacb --- /dev/null +++ b/psf-memo-db/test/unit/use-cases/list-following.unit.js @@ -0,0 +1,45 @@ +/* + Unit tests for the ListFollowing use case. +*/ + +import { assert } from 'chai' +import ListFollowing from '../../../src/use-cases/list-following.js' + +function makeAdapters (following) { + return { + followQuery: { + async listFollowing (followerAddr) { + return following + } + } + } +} + +describe('#ListFollowing', () => { + it('should throw when adapters is missing', () => { + assert.throws(() => new ListFollowing({}), /Adapters required/) + }) + + it('should throw when followQuery adapter is missing', () => { + assert.throws(() => new ListFollowing({ adapters: {} }), /followQuery adapter required/) + }) + + it('should return the following list', async () => { + const useCase = new ListFollowing({ adapters: makeAdapters(['bitcoincash:a', 'bitcoincash:b']) }) + const result = await useCase.execute({ followerAddr: 'bitcoincash:follower' }) + assert.deepEqual(result, { + followerAddr: 'bitcoincash:follower', + following: ['bitcoincash:a', 'bitcoincash:b'] + }) + }) + + it('should reject a missing followerAddr', async () => { + const useCase = new ListFollowing({ adapters: makeAdapters([]) }) + try { + await useCase.execute({}) + assert.fail('expected error') + } catch (err) { + assert.match(err.message, /followerAddr is required/) + } + }) +}) From d9ba5a2774a0e0485d411bcb0f805c25f6c0346b Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 27 Aug 2026 10:46:39 -0700 Subject: [PATCH 4/4] Refactor follow use-cases and profile page; add follow property tests Share the follow list use-case contract through a FollowListUseCase base and delegate construction validation to the existing ListUseCase, removing the duplicated follow use-case constructors and execute bodies. Extract ProfilePage.load helpers and a shared follow/unfollow setter to reduce CRAP below the threshold and remove duplicated follow logic. Add property tests covering MemoFollow broadcast conservation and hash160 length, the FollowQuery address round trip, and isFollowing/listFollowing/listFollowers consistency over random record sets. By refactorer. --- psf-memo-client/src/services/profile-page.js | 51 +++--- .../test/property/follow.property.test.js | 132 +++++++++++++++ psf-memo-db/src/use-cases/follow-state.js | 13 +- .../src/use-cases/lib/follow-list-use-case.js | 30 ++++ psf-memo-db/src/use-cases/list-followers.js | 29 +--- psf-memo-db/src/use-cases/list-following.js | 29 +--- .../test/property/follow.property.test.js | 158 ++++++++++++++++++ 7 files changed, 372 insertions(+), 70 deletions(-) create mode 100644 psf-memo-client/test/property/follow.property.test.js create mode 100644 psf-memo-db/src/use-cases/lib/follow-list-use-case.js create mode 100644 psf-memo-db/test/property/follow.property.test.js diff --git a/psf-memo-client/src/services/profile-page.js b/psf-memo-client/src/services/profile-page.js index 292c3f3..b0c7e16 100644 --- a/psf-memo-client/src/services/profile-page.js +++ b/psf-memo-client/src/services/profile-page.js @@ -25,22 +25,12 @@ class ProfilePage { } async load ({ limit = 100, offset = 0 } = {}) { - if (!this.memoDb) { - throw new Error('Profile page requires a memo db client.') - } - if (!this.addr) { - throw new Error('Profile page requires an address.') - } + this._assertReady() const data = await this.memoDb.getPostsByAddr(this.addr, { limit, offset }) this.posts = data.posts || [] this.pagination = data.pagination || null - - if (this.myAddr && !this.isOwnProfile()) { - this.followState = await this.memoDb.getFollowState(this.myAddr, this.addr) - } else { - this.followState = false - } + this.followState = await this._loadFollowState() return { posts: this.posts, @@ -50,6 +40,25 @@ class ProfilePage { } } + // Throw unless the injected dependencies and target address are present. + _assertReady () { + if (!this.memoDb) { + throw new Error('Profile page requires a memo db client.') + } + if (!this.addr) { + throw new Error('Profile page requires an address.') + } + } + + // Fetch the viewer's follow state for the target address, or false when + // there is no viewer or the profile is the viewer's own. + async _loadFollowState () { + if (this.myAddr && !this.isOwnProfile()) { + return this.memoDb.getFollowState(this.myAddr, this.addr) + } + return false + } + isOwnProfile () { return Boolean(this.myAddr) && this.myAddr === this.addr } @@ -63,20 +72,20 @@ class ProfilePage { } async follow () { - if (!this.memoFollow) { - throw new Error('Profile page requires a memo follow handler.') - } - await this.memoFollow.follow(this.addr) - this.followState = true - return { ok: true } + return this._setFollowState('follow', true) } async unfollow () { + return this._setFollowState('unfollow', false) + } + + // Delegate follow/unfollow to the injected handler and reflect the new state. + async _setFollowState (method, nextState) { if (!this.memoFollow) { throw new Error('Profile page requires a memo follow handler.') } - await this.memoFollow.unfollow(this.addr) - this.followState = false + await this.memoFollow[method](this.addr) + this.followState = nextState return { ok: true } } @@ -90,5 +99,5 @@ ProfilePage.PROFILE_PATH_PREFIX = PROFILE_PATH_PREFIX module.exports = ProfilePage // mutate4javascript-manifest-begin -// {"version":1,"tested_at":"2026-08-27T03:38:35.169Z","module_hash":"3d7b866f2997caa538addbc83086aec9b697f318a83cdc976d7f976cefa38e99","functions":[{"id":"func/ProfilePage.constructor","name":"ProfilePage.constructor","line":16,"end_line":21,"hash":"7c71c8c700d9f388b7a6904b8b3fae0f4c50e23288e96ac90e26f89797c16136"},{"id":"func/ProfilePage.load","name":"ProfilePage.load","line":23,"end_line":36,"hash":"6ccf9d1559cc12ba56d876cf68c2b0f369f195ec2d1058451c0b3bb9dcd3a515"},{"id":"func/ProfilePage.getPost","name":"ProfilePage.getPost","line":38,"end_line":40,"hash":"1a6ae1a02b0f79b5b62a2b2324a5f1edbd743bec004fe73cfef7970bead56885"}]} +// {"version":1,"tested_at":"2026-08-27T17:41:02.811Z","module_hash":"556ad5297067de3e39ddbb028a87d502cc0a9ab6525b1bba2c3d36c669207e1d","functions":[{"id":"func/ProfilePage.constructor","name":"ProfilePage.constructor","line":17,"end_line":25,"hash":"5e8bd826d8059e2d86aa3064c7b7d3041d5cf2fff7eb8942976bda600d2812a2"},{"id":"func/ProfilePage.load","name":"ProfilePage.load","line":27,"end_line":41,"hash":"e6c81ab6dbfda80903b134bdfea626d7e82e567780fd577d7a78e6dd44a22f37"},{"id":"func/ProfilePage._assertReady","name":"ProfilePage._assertReady","line":44,"end_line":51,"hash":"e575f98fa1b492b4c11ae5275459d9f9996df4c566b84c7cae91413c72a02f38"},{"id":"func/ProfilePage._loadFollowState","name":"ProfilePage._loadFollowState","line":55,"end_line":60,"hash":"65ff871041cab36b5e45acad5c8ca904c411af197f77dbb528e31ac5666668b3"},{"id":"func/ProfilePage.isOwnProfile","name":"ProfilePage.isOwnProfile","line":62,"end_line":64,"hash":"4d149bfe7b183b5d38d496c0a8126d22bd9a919e684f375d66e2c8c1348dd773"},{"id":"func/ProfilePage.canFollow","name":"ProfilePage.canFollow","line":66,"end_line":68,"hash":"0bc2c0c17991d8f1a1d18fba29c901e42633b7cf2c2c8b5747a49e5832d7001d"},{"id":"func/ProfilePage.isFollowing","name":"ProfilePage.isFollowing","line":70,"end_line":72,"hash":"9fc0470db7ffea2da96d2dbbdceff55562e1f7e84377fbb0e8b60d53cc723b40"},{"id":"func/ProfilePage.follow","name":"ProfilePage.follow","line":74,"end_line":76,"hash":"7674b789a9d3c48e0f7a6e553613bac99b2f38449fcdd17faa3c6d7cd2773bdd"},{"id":"func/ProfilePage.unfollow","name":"ProfilePage.unfollow","line":78,"end_line":80,"hash":"69d278b09da1f284be6adacb7264f9c06ac71c42f8e8f1baf9e34d5b24a891fb"},{"id":"func/ProfilePage._setFollowState","name":"ProfilePage._setFollowState","line":83,"end_line":90,"hash":"8eda1a436468b17327e8e20df4f84bf0263591f320c494f2c74c1b5be21b2ad8"},{"id":"func/ProfilePage.getPost","name":"ProfilePage.getPost","line":92,"end_line":94,"hash":"1a6ae1a02b0f79b5b62a2b2324a5f1edbd743bec004fe73cfef7970bead56885"}]} // mutate4javascript-manifest-end diff --git a/psf-memo-client/test/property/follow.property.test.js b/psf-memo-client/test/property/follow.property.test.js new file mode 100644 index 0000000..b795192 --- /dev/null +++ b/psf-memo-client/test/property/follow.property.test.js @@ -0,0 +1,132 @@ +/* + Property tests for the Memo follow/unfollow behavior. + + The unit tests probe the broadcast at a couple of fixed addresses. These + properties cover broad input ranges so the invariants hold everywhere: + + - conservation: follow() and unfollow() each broadcast exactly one Memo + action carrying the followee's 20-byte hash160 payload. + - hash length: the broadcast payload is always exactly PK_HASH_LENGTH + bytes (a hash160). + - round trip: follow then unfollow toggles the reflected follow state + back to false, and vice versa. +*/ + +'use strict' + +const test = require('node:test') +const crypto = require('node:crypto') +const { seededRandom, forAll, intGen } = require('./harness') +const MemoFollow = require('../../src/services/memo-follow') + +const rng = seededRandom(20260830) + +const MY_ADDRESS = 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d' +const CHARS = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l' + +// Deterministic 20-byte hash160 hex for any input string, mirroring what a +// real wallet's bch-js produces for a valid cash address. +function hash20 (s) { + return crypto.createHash('sha256').update(s).digest('hex').slice(0, 40) +} + +// Random cash-address-shaped string (never empty so it passes validation). +function addressGen () { + const len = intGen(rng, 40, 60)() + let out = 'bitcoincash:q' + for (let i = 0; i < len; i++) { + out += CHARS[Math.floor(rng() * CHARS.length)] + } + return out +} + +function makeBchjs () { + return { + Address: { + toHash160 (addr) { + return hash20(addr) + } + } + } +} + +function makeWallet (address = MY_ADDRESS) { + return { + walletInfo: { cashAddress: address }, + bchjs: makeBchjs(), + broadcasts: [], + async getUtxos () { + return [] + }, + async sendOpReturn (msg, prefix) { + this.broadcasts.push({ msg, prefix }) + return 'aa'.repeat(32) + } + } +} + +function makeProfiles () { + const state = {} + return { + setFollowState: (selfAddr, targetAddr, isFollowing) => { + if (!state[selfAddr]) state[selfAddr] = {} + state[selfAddr][targetAddr] = isFollowing + }, + getFollowState: (selfAddr, targetAddr) => state[selfAddr]?.[targetAddr] || false + } +} + +test('follow broadcasts exactly one hash160 payload with the follow prefix', async () => { + await forAll( + (i) => addressGen(), + async (addr) => { + const wallet = makeWallet() + const memoFollow = new MemoFollow({ wallet }) + await memoFollow.follow(addr) + + return wallet.broadcasts.length === 1 && + wallet.broadcasts[0].prefix === MemoFollow.MEMO_FOLLOW_PREFIX && + Buffer.isBuffer(wallet.broadcasts[0].msg) && + wallet.broadcasts[0].msg.length === MemoFollow.PK_HASH_LENGTH && + wallet.broadcasts[0].msg.toString('hex') === hash20(addr) + }, + { label: 'follow broadcast conservation and hash160 length' } + ) +}) + +test('unfollow broadcasts exactly one hash160 payload with the unfollow prefix', async () => { + await forAll( + (i) => addressGen(), + async (addr) => { + const wallet = makeWallet() + const memoFollow = new MemoFollow({ wallet }) + await memoFollow.unfollow(addr) + + return wallet.broadcasts.length === 1 && + wallet.broadcasts[0].prefix === MemoFollow.MEMO_UNFOLLOW_PREFIX && + Buffer.isBuffer(wallet.broadcasts[0].msg) && + wallet.broadcasts[0].msg.length === MemoFollow.PK_HASH_LENGTH && + wallet.broadcasts[0].msg.toString('hex') === hash20(addr) + }, + { label: 'unfollow broadcast conservation and hash160 length' } + ) +}) + +test('follow then unfollow round-trips the reflected follow state', async () => { + await forAll( + (i) => addressGen(), + async (addr) => { + const wallet = makeWallet() + const profiles = makeProfiles() + const memoFollow = new MemoFollow({ wallet, profiles }) + + await memoFollow.follow(addr) + const afterFollow = profiles.getFollowState(MY_ADDRESS, addr) + await memoFollow.unfollow(addr) + const afterUnfollow = profiles.getFollowState(MY_ADDRESS, addr) + + return afterFollow === true && afterUnfollow === false + }, + { label: 'follow/unfollow reflected state round trip' } + ) +}) diff --git a/psf-memo-db/src/use-cases/follow-state.js b/psf-memo-db/src/use-cases/follow-state.js index 0ad59f4..ec89eca 100644 --- a/psf-memo-db/src/use-cases/follow-state.js +++ b/psf-memo-db/src/use-cases/follow-state.js @@ -4,16 +4,11 @@ Returns { followerAddr, followeeAddr, following: boolean }. */ -class FollowState { +import { ListUseCase } from './lib/use-case.js' + +class FollowState extends ListUseCase { constructor (localConfig = {}) { - this.adapters = localConfig.adapters - if (!this.adapters) { - throw new Error('Adapters required when instantiating FollowState use case.') - } - if (!this.adapters.followQuery) { - throw new Error('followQuery adapter required for FollowState use case.') - } - this.execute = this.execute.bind(this) + super(localConfig, { useCaseName: 'FollowState', adapterName: 'followQuery' }) } async execute (inObj = {}) { diff --git a/psf-memo-db/src/use-cases/lib/follow-list-use-case.js b/psf-memo-db/src/use-cases/lib/follow-list-use-case.js new file mode 100644 index 0000000..2f8e2ab --- /dev/null +++ b/psf-memo-db/src/use-cases/lib/follow-list-use-case.js @@ -0,0 +1,30 @@ +/* + Shared construction and execution contract for the follow list use cases + (list-following, list-followers). + + Both follow list use cases validate a single address field, call the matching + FollowQuery list method, and return that address alongside the result list. + Centralizing this removes the per-class constructor and execute boilerplate + that the two endpoints previously duplicated. Construction validation is + delegated to ListUseCase. +*/ + +import { ListUseCase } from './use-case.js' + +export class FollowListUseCase extends ListUseCase { + constructor (localConfig, { useCaseName, adapterMethod, addrField, resultField }) { + super(localConfig, { useCaseName, adapterName: 'followQuery' }) + this.addrField = addrField + this.resultField = resultField + this._list = this.adapters.followQuery[adapterMethod].bind(this.adapters.followQuery) + } + + async execute (inObj = {}) { + const addr = inObj[this.addrField] + if (!addr || typeof addr !== 'string') { + throw new Error(`${this.addrField} is required`) + } + const list = await this._list(addr) + return { [this.addrField]: addr, [this.resultField]: list } + } +} diff --git a/psf-memo-db/src/use-cases/list-followers.js b/psf-memo-db/src/use-cases/list-followers.js index 312ee57..a17dc9e 100644 --- a/psf-memo-db/src/use-cases/list-followers.js +++ b/psf-memo-db/src/use-cases/list-followers.js @@ -4,27 +4,16 @@ Returns { followeeAddr, followers: string[] }. */ -class ListFollowers { +import { FollowListUseCase } from './lib/follow-list-use-case.js' + +class ListFollowers extends FollowListUseCase { constructor (localConfig = {}) { - this.adapters = localConfig.adapters - if (!this.adapters) { - throw new Error('Adapters required when instantiating ListFollowers use case.') - } - if (!this.adapters.followQuery) { - throw new Error('followQuery adapter required for ListFollowers use case.') - } - this.execute = this.execute.bind(this) - } - - async execute (inObj = {}) { - const { followeeAddr } = inObj - if (!followeeAddr || typeof followeeAddr !== 'string') { - throw new Error('followeeAddr is required') - } - - const followers = await this.adapters.followQuery.listFollowers(followeeAddr) - - return { followeeAddr, followers } + super(localConfig, { + useCaseName: 'ListFollowers', + adapterMethod: 'listFollowers', + addrField: 'followeeAddr', + resultField: 'followers' + }) } } diff --git a/psf-memo-db/src/use-cases/list-following.js b/psf-memo-db/src/use-cases/list-following.js index 1efe85f..cbcfd3a 100644 --- a/psf-memo-db/src/use-cases/list-following.js +++ b/psf-memo-db/src/use-cases/list-following.js @@ -4,27 +4,16 @@ Returns { followerAddr, following: string[] }. */ -class ListFollowing { +import { FollowListUseCase } from './lib/follow-list-use-case.js' + +class ListFollowing extends FollowListUseCase { constructor (localConfig = {}) { - this.adapters = localConfig.adapters - if (!this.adapters) { - throw new Error('Adapters required when instantiating ListFollowing use case.') - } - if (!this.adapters.followQuery) { - throw new Error('followQuery adapter required for ListFollowing use case.') - } - this.execute = this.execute.bind(this) - } - - async execute (inObj = {}) { - const { followerAddr } = inObj - if (!followerAddr || typeof followerAddr !== 'string') { - throw new Error('followerAddr is required') - } - - const following = await this.adapters.followQuery.listFollowing(followerAddr) - - return { followerAddr, following } + super(localConfig, { + useCaseName: 'ListFollowing', + adapterMethod: 'listFollowing', + addrField: 'followerAddr', + resultField: 'following' + }) } } diff --git a/psf-memo-db/test/property/follow.property.test.js b/psf-memo-db/test/property/follow.property.test.js new file mode 100644 index 0000000..84e7d6b --- /dev/null +++ b/psf-memo-db/test/property/follow.property.test.js @@ -0,0 +1,158 @@ +/* + Property tests for the FollowQuery adapter. + + The unit tests probe isFollowing/listFollowing/listFollowers at a few fixed + fixtures. These properties cover broad random record sets so the invariants + hold everywhere: + + - round trip: a hash160 converts to a cash address and back to the same + hash160. + - isFollowing conservation: an active follow record reports true and an + unfollow record reports false, regardless of surrounding records. + - list consistency: listFollowing and listFollowers each return exactly + the active relationships in their direction. +*/ + +import test from 'node:test' +import BCHJS from '@psf/bch-js' +import { seededRandom, forAll, intGen } from './harness.js' +import FollowQuery from '../../src/adapters/follow-query.js' + +const rng = seededRandom(20260830) +const bchjs = new BCHJS({ restURL: 'https://api.fullstack.cash/v5/' }) + +const HEX = '0123456789abcdef' + +function hash160Gen () { + let out = '' + for (let i = 0; i < 40; i++) { + out += HEX[Math.floor(rng() * HEX.length)] + } + return out +} + +// An in-memory follows Db mirroring the LevelDB contract FollowQuery relies on. +function makeFollowsDb (records) { + const store = new Map(records.map((r) => [r.key, r])) + return { + async get (key) { + if (!store.has(key)) { + const err = new Error('not found') + err.notFound = true + throw err + } + return store.get(key) + }, + iterator (opts = {}) { + const entries = Array.from(store.entries()).sort((a, b) => a[0].localeCompare(b[0])) + const { gte, lt } = opts + const filtered = entries.filter(([key]) => { + if (gte && key < gte) return false + if (lt && key >= lt) return false + return true + }) + let i = 0 + return { + [Symbol.asyncIterator] () { + return this + }, + async next () { + if (i >= filtered.length) return { value: undefined, done: true } + const entry = filtered[i++] + return { value: entry, done: false } + }, + async close () {} + } + } + } +} + +// Build a random set of follow records: a mix of followers, hash160s, and +// follow/unfollow flags. Not every pair is recorded. +function recordSetGen () { + const followers = [] + for (let i = 0, n = intGen(rng, 1, 6)(); i < n; i++) followers.push(`bitcoincash:q${hash160Gen()}`) + const hash160s = [] + for (let i = 0, n = intGen(rng, 1, 6)(); i < n; i++) hash160s.push(hash160Gen()) + + const records = [] + for (const follower of followers) { + for (const hash160 of hash160s) { + if (rng() < 0.4) continue + records.push({ key: `${follower}:${hash160}`, unfollow: rng() < 0.5 }) + } + } + return { followers, hash160s, records } +} + +test('hash160 to cash address and back round-trips to the same hash160', async () => { + await forAll( + (i) => hash160Gen(), + (hash160) => { + const cash = bchjs.Address.hash160ToCash(hash160) + return bchjs.Address.toHash160(cash) === hash160 + }, + { label: 'hash160 <-> cash address round trip' } + ) +}) + +test('isFollowing is true exactly for active follow records', async () => { + await forAll( + (i) => recordSetGen(), + async ({ records }) => { + const query = new FollowQuery({ followsDb: makeFollowsDb(records), bchjs }) + for (const record of records) { + // Follower cash addresses contain a colon ('bitcoincash:q...'), so the + // record key has two colons; the hash160 is the trailing segment. + const sep = record.key.lastIndexOf(':') + const follower = record.key.slice(0, sep) + const hash160 = record.key.slice(sep + 1) + const followee = bchjs.Address.hash160ToCash(hash160) + const following = await query.isFollowing(follower, followee) + if (following !== (record.unfollow !== true)) return false + } + return true + }, + { label: 'isFollowing conservation' } + ) +}) + +test('listFollowing returns exactly the active followees for a follower', async () => { + await forAll( + (i) => recordSetGen(), + async ({ followers, records }) => { + const query = new FollowQuery({ followsDb: makeFollowsDb(records), bchjs }) + for (const follower of followers) { + const prefix = `${follower}:` + const expected = records + .filter((r) => r.key.startsWith(prefix) && r.unfollow !== true) + .map((r) => bchjs.Address.hash160ToCash(r.key.slice(prefix.length))) + const got = (await query.listFollowing(follower)).sort() + const want = Array.from(new Set(expected)).sort() + if (JSON.stringify(got) !== JSON.stringify(want)) return false + } + return true + }, + { label: 'listFollowing consistency' } + ) +}) + +test('listFollowers returns exactly the active followers for a followee', async () => { + await forAll( + (i) => recordSetGen(), + async ({ hash160s, records }) => { + const query = new FollowQuery({ followsDb: makeFollowsDb(records), bchjs }) + for (const hash160 of hash160s) { + const suffix = `:${hash160}` + const expected = records + .filter((r) => r.key.endsWith(suffix) && r.unfollow !== true) + .map((r) => r.key.slice(0, r.key.length - suffix.length)) + const got = (await query.listFollowers(bchjs.Address.hash160ToCash(hash160))).sort() + const want = Array.from(new Set(expected)).sort() + if (JSON.stringify(got) !== JSON.stringify(want)) return false + } + return true + }, + { label: 'listFollowers consistency' } + ) +})