Skip to content

feat(wallets): per-cosigner message signing across keystores - #511

Closed
bucko13 wants to merge 33 commits into
caravan-bitcoin:mainfrom
bucko13:feat/message-signing-keystore-support
Closed

feat(wallets): per-cosigner message signing across keystores#511
bucko13 wants to merge 33 commits into
caravan-bitcoin:mainfrom
bucko13:feat/message-signing-keystore-support

Conversation

@bucko13

@bucko13 bucko13 commented May 21, 2026

Copy link
Copy Markdown
Contributor

Adds SignMessage({keystore, network?, bip32Path, message, expectedPubkey}) across Ledger v1+v2, Trezor, Jade, BitBox, and Coldcard. All keystores return a normalized Entry = {bip32Path, signature, expectedPubkey} and a single verifyMessageSignature (loose-mode bip322-js) handles the recovery side.

This only implements BIP137 support as most devices still don't have any BIP322 support. I might put up a follow up that covers BIP322 for coldcard which recently added support.

Also wires the new flow into /#/test as a Message Signing category over TEST_FIXTURES.multisigs.

Note: @caravan/wallets bumps to 1.0.0 due to the backwards incompatible changes to the existing interactions normalizing the return string.

What's left before merging

Need to validate in the test suite:

  • Ledger
  • Trezor
  • Jade
    - [x] Bitbox not actually supported
  • Coldcard

Not included:

  • BCUR2 since it doesn't look like BCUR defines a registry type for message signing and implementation might require more research and bigger scope, I punted for now
  • BIP322 support: there's not a lot of support yet so didn't seem like a priority for first pass
  • Bitbox: only seems to support message signing for non multisig paths. would need to talk to bitbox team about being more flexible for message signing paths so that you can validate keys from a multisig wallet using the BIP137 standard (which is only P2WPKH)

@changeset-bot

changeset-bot Bot commented May 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 948c273

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@caravan/bitcoin Patch
caravan-coordinator Minor
@caravan/wallets Major
@caravan/psbt Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented May 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
caravan-coordinator Ready Ready Preview, Comment May 21, 2026 9:44pm

Request Review

bucko13 added 22 commits May 21, 2026 14:50
Adds packages/caravan-wallets/src/messages.ts with:

- Entry: canonical record returned by each keystore's
  SignMessage.run() (bip32Path, signature, legacy, expectedPubkey).
  Self-describing so the verifier does not need per-keystore
  knowledge.

- verifyMessageSignature({message, entry}): pubkey-aware verifier
  wrapping bip322-js in loose mode. Derives a canonical P2WPKH
  address from entry.expectedPubkey. Loose mode is required because
  BIP-322 spec prohibits BIP-137 over P2WPKH, and caravan's cosigner
  paths in P2WSH multisig wallets canonicalize to P2WPKH; strict
  mode would reject every Ledger / Trezor / Jade output against
  such a path.

- validateMessage: UTF-8 + no NUL + <=240-byte encoding gate.
  Whitespace handling is left to UI surfaces since on-device approval
  screens render whitespace invisibly.

- MessageSigningError: closed-set discriminated union covering
  UnsupportedProtocol, UnsupportedAddressType, DeviceRejected,
  TransportError, MalformedResponse, MalformedRequest.

- NormalizeSignature, CapabilityFlags: contracts each per-keystore
  SignMessage interaction will implement.

Tests cover encoding policy, single-path verifier round-trips
(BIP-322 Simple + BIP-137 in loose mode) with negatives, and
per-fixture round-trips across TEST_FIXTURES.multisigs (P2SH /
P2SH-P2WSH / P2WSH x testnet/mainnet). Each fixture also exercises
a "claim unchained pubkey" negative using publicKeys[]; the
unchained seed is not in TEST_FIXTURES, same constraint the existing
PSBT signingTests operate under.

bip322-js added as @caravan/wallets dependency; @scure/bip32,
@scure/bip39, bitcoinjs-message, wif as devDependencies for tests.
bip322-js@3 transitively pulls bitcoinjs-lib@6 under its subtree;
the top-level bitcoinjs-lib@5 dep is unaffected.
…tedPubkey

LedgerSignMessage changes:

- Constructor accepts expectedPubkey (required).
- Validates the message via validateMessage from messages.ts
  (UTF-8, no NUL, <=240 bytes). Throws MessageSigningError
  {MalformedRequest} on policy violations before contacting the
  device.
- .run() returns Promise<Entry>. Normalizes Ledger's {v,r,s}
  response to a base64 BIP-137 65-byte signature (header byte =
  v + 27 + 4, then r||s) via a new normalizeLedgerSignature helper.
  caravan's loose-mode verifier ignores the address-type bits in
  the header.
- Fixes a latent bug: the previous code called
  app.signMessageNew(...), which does not exist in
  @ledgerhq/hw-app-btc. The SDK exposes app.signMessage(path,
  messageHex). Hex-encodes the UTF-8 message before the SDK call.

This class is the BIP-137 implementation for Ledger.

SignMessage factory:

- Adds expectedPubkey (required) to the input type. Only the
  LEDGER case threads it; JADE / TREZOR cases will be adapted in
  follow-up commits.

Tests:

- Constructor throw on oversize message.
- End-to-end .run() with mocked transport asserting the SDK call
  shape (positional path + hex-encoded message) and the resulting
  Entry fields.
TrezorSignMessage changes:

- Constructor accepts expectedPubkey (required) plus network so the
  base TrezorInteraction can resolve coin code.
- Validates the message via validateMessage (UTF-8, no NUL,
  <=240 bytes).
- parsePayload now returns Entry. Trezor's SDK returns
  {address, signature}; address is ignored — caravan verifies
  against expectedPubkey, not the device-derived address.

Fixes a latent bug: the base class previously called
`super({ network: Network[network] })`. `Network` is a string enum,
so the reverse-lookup is always undefined and `trezorCoin()` always
fell into the testnet branch. Pass `network` through unchanged.

SignMessage factory:

- Adds optional `network` param. Threaded for TREZOR.
- Threads expectedPubkey for TREZOR.

Tests:

- Constructor throw on oversize message.
- connectParams returns the expected (method, params) shape with
  the right coin label for mainnet vs testnet.
- parsePayload maps {address, signature} → Entry.
…byte

JadeSignMessage changes:

- Constructor accepts expectedPubkey (required).
- Validates the message via validateMessage.
- .run() returns Entry. Jade's SDK emits a raw 64-byte EC signature
  with no recovery byte (the firmware exposes the sig without
  protocol-level conformance to BIP-137 or BIP-322), so the
  interaction reconstructs the canonical BIP-137 wire form.

normalizeJadeSignature:

- Tries both `v` candidates ({0, 1}) by building
  [v + 27 + 4][r][s] and verifying against expectedPubkey via the
  loose-mode verifier in messages.ts.
- Throws MessageSigningError{MalformedResponse} if neither candidate
  recovers. This doubles as a guard against a wrong wallet loaded on
  the device.
- Guards against the anti-exfil tuple shape: when useAeSignatures is
  enabled the SDK returns [sig, hostCommitment]; caravan does not
  request anti-exfil mode, so the tuple shape is unexpected and the
  normalizer throws MalformedResponse rather than misinterpreting
  the bytes.

SignMessage factory:

- Threads expectedPubkey for JADE.

Tests:

- Constructor throw on oversize message.
- Raw sig of wrong length → MalformedResponse.
- Anti-exfil tuple shape → MalformedResponse.
- Sig that does not recover to expectedPubkey under either v →
  MalformedResponse.
- Happy-path round-trip: synthesize a Jade-style raw EC sig (strip
  the header byte off a bitcoinjs-message BIP-137 sig), confirm
  .run() reconstructs a canonical Entry that verifies.
…eystore

LedgerSignMessage now dispatches internally on app generation. The
class already supported the legacy Bitcoin app via
@ledgerhq/hw-app-btc; this commit adds support for the v2 Bitcoin app
exposed by ledger-bitcoin's AppClient.

Run-loop changes:

- After isAppSupported() / isLegacyApp() resolve, the run path picks
  between Btc.signMessage(path, messageHex) (legacy, returns {v,r,s}
  — normalizer wraps as [v+31][r][s] base64) and
  AppClient.signMessage(messageBuf, path) (v2, returns base64
  BIP-137 directly — note the reversed positional order vs the
  legacy SDK).
- Sets isV2Supported = true. Was previously a placeholder.

SignMessage factory:

- LEDGER and LEDGER_V2 both route to LedgerSignMessage; the class
  detects the app version at .run() time.

Tests:

- Mocked legacy-app path: assert SDK call shape and normalized sig.
- Mocked v2-app path: assert SDK call shape (reversed args) and
  pass-through base64.
Wires per-cosigner message signing into the in-app keystore test
runner so contributors with a physical device can validate the new
SignMessage factory + verifier end to end.

apps/coordinator/src/tests/messageSigning.jsx:

- For each TEST_FIXTURES.multisigs fixture, build a MessageSigningTest
  bound to the open_source cosigner's expected pubkey at the
  fixture's bip32Path.
- Test description renders address type, message, BIP-32 path,
  expected pubkey, and protocol (BIP-137 loose-mode verification).
- interaction() calls SignMessage({keystore, network, bip32Path,
  message, expectedPubkey}).
- matches() invokes verifyMessageSignature cryptographically — not
  byte equality — because BIP-322 Simple is non-deterministic at the
  wire level.

apps/coordinator/src/tests/index.js:

- Adds messageSigningTests to the test category lookup, keyed on
  TYPES.MESSAGE_SIGNING.
- Threads the category through buildTests / fixturesForType so the
  /#/test UI renders it alongside the existing signing tests.
- Wired for direct keystores: LEDGER, LEDGER_V2, TREZOR, JADE,
  BITBOX. COLDCARD's indirect (SD-card) flow lands in a separate
  commit alongside the keystore class. BCUR2 / Hermit / Custom are
  excluded.

Coordinator tests pass; e2e does not exercise this surface since it
requires a physical device.
Adds BitBoxSignMessage to the BitBox keystore driver, completing
SignMessage coverage for direct-transport keystores.

BitBoxSignMessage:

- Constructor: requires network (for BitBox's coin code via
  convertNetwork — btc/tbtc/rbtc), bip32Path, message, expectedPubkey.
  Validates the message via validateMessage.
- .run() calls pairedBitBox.btcSignMessage(coin, {scriptConfig:
  {simpleType: 'p2wpkh'}, keypath}, TextEncoder().encode(message))
  inside withDevice. Returns Entry.
- BitBox returns {sig, recid, electrumSig65}. electrumSig65 is
  already the canonical 65-byte BIP-137 wire form (header + r + s)
  with the Electrum header convention; caravan base64-encodes it
  directly. Loose-mode verification tolerates any header.
- Validates the response: throws MessageSigningError{MalformedResponse}
  if electrumSig65 is missing or not exactly 65 bytes.

SignMessage factory:

- Threads network for BITBOX. Throws MessageSigningError
  {MalformedRequest} if network is missing — BitBox cannot sign
  without a coin code.

Tests:

- Constructor throw on oversize message.
- Mocked happy path: assert btcSignMessage SDK call shape and the
  resulting Entry.
- MalformedResponse on a wrong-length electrumSig65.
- Coin code selection on mainnet/testnet.
Adds ColdcardSignMessage to the Coldcard keystore driver. Coldcard
is an indirect keystore, so the interaction is file-based rather
than transport-based.

ColdcardSignMessage extends ColdcardInteraction
(IndirectKeystoreInteraction):

- workflow = ["request", "parse"] — both indirect phases are required.
- request(): builds the 3-line .txt Coldcard expects on SD card:
    {message}\n{bip32Path}\np2wpkh\n
- parse(file): consumes Coldcard's armored "Bitcoin Signed Message"
  output, extracts the base64 signature from the line after the
  address line under BEGIN SIGNATURE, returns Entry.
- messages(): PENDING messages walk the user through "Download +
  save to SD card → Advanced > File Management > Sign Text File on
  Coldcard → upload signed file".
- Validates the message via validateMessage at construction time.
  Throws MessageSigningError{MalformedResponse} on empty/garbage
  uploads.

BIP-322 mode is intentionally not supported on this class. Coldcard's
BIP-322 firmware path is the Proof-of-Reserve PSBT flow (Mk 5.5.0 /
Q 1.4.0Q+), which proves wallet-level UTXO control via the BIP-322
FULL form — a different use case from per-cosigner-key BIP-322 Simple.
A future ColdcardSignMessageBIP322 can wrap the PoR PSBT flow if
caravan needs that capability; this class implements BIP-137 only.

SignMessage factory:

- Wires the COLDCARD case to ColdcardSignMessage.

Coordinator:

- ColdcardSignMessage now appears in the messageSigning test
  category via the standard indirect-keystore wiring in
  apps/coordinator/src/tests/index.js.

Tests:

- Constructor throw on oversize message.
- request() returns the canonical 3-line .txt.
- parse() extracts the base64 sig from a canonical armored file.
- parse() throws MalformedResponse on empty input, missing
  delimiters, and missing address+signature lines.

BCUR2 message signing is deferred — the UR convention is unresolved.
Documents the message-signing API surface introduced across the
preceding implementation commits.

packages/caravan-wallets/CLAUDE.md SignMessage section:

- Documents the canonical Entry shape and verifier behaviour.
- Lists per-keystore capabilities (legacy + bip322 flags),
  noting Coldcard's BIP-322 PoR PSBT path as a future separate
  class.
- Calls out the bitcoinjs-lib v5 vs v6 alignment under bip322-js.
- Calls out the Trezor `Network[network]` reverse-lookup bug fix
  that landed alongside TrezorSignMessage.

.changeset/message-signing-keystore-support.md:

- @caravan/wallets bumped to major (0.10.1 → 1.0.0). The package is
  broadly deployed and existing callers receive `.run()` return
  shapes that have changed (Ledger {v,r,s}, Trezor
  {address,signature}, Jade raw hex EC sig → now all return Entry).
  The factory's expectedPubkey argument is also newly required.
- Drop unused NormalizeSignature export — the per-keystore
  normalizers diverge in signature (Jade needs `message` too) and
  no caller uses the type. Future shape can re-introduce if a real
  interface emerges.
- Drop UnsupportedAddressType from MessageSigningErrorKind — never
  thrown anywhere. Verifier returns false for unsupported pubkey
  shapes; pre-device address-type checks aren't in scope.
- Differentiate MessageSigningError.message vs userMessage:
  e.message now carries a structured `[Kind/KEYSTORE]` prefix for
  logs; e.userMessage stays bare for UI surfaces.
- verifyMessageSignature now takes `{message, signature,
  expectedPubkey}` directly instead of an Entry wrapper. Drops the
  awkward fabricated `bip32Path: ""` in Jade's internal recovery-
  byte search and gives non-Entry callers a clean entry point.
- Lift validateMessage into the SignMessage factory (single call
  site before the switch) and drop it from each per-keystore
  constructor. Centralizes the "throw before contacting any device"
  contract and removes five copies of the same call.
- Comment MAX_MESSAGE_BYTES = 240 with the Coldcard SD-card
  justification.
- Replace per-keystore "constructor throws on oversize" tests (five
  near-duplicates) with one factory-level test that loops over all
  supported keystores, asserting each surfaces MalformedRequest with
  the right `keystore` label. Catches a future keystore added to
  the factory without going through validateMessage.
Every error reachable from SignMessage.run() now surfaces as
MessageSigningError so consumers can branch on `e.kind` without
falling through to raw SDK exceptions.

`wrapSdkError(keystore, err)` is the shared classifier. It passes
existing MessageSigningError instances through untouched (so
MalformedResponse / MalformedRequest from the keystore layer aren't
clobbered), then uses a lenient heuristic to label the rest as
DeviceRejected (Ledger statusCode 0x6985; "cancel" / "reject" /
"denied" / "declined" / "abort" in the message) or TransportError
(everything else). Over-tagging a transport drop as DeviceRejected
is preferred over the reverse — the user-facing string is still
sensible the wrong way for a transport drop, but confusing the
wrong way for a cancellation.

Call sites:

- LedgerSignMessage.run() wraps the SDK call (both legacy and v2
  app branches).
- TrezorSignMessage.run() overrides the base class run() so that
  the raw `Error(result.payload.error)` thrown for `result.success
  === false` (cancellations, timeouts, device errors) is
  reclassified.
- BitBoxSignMessage.run() wraps btcSignMessage; MalformedResponse
  for non-65-byte electrumSig65 still throws unchanged.
- JadeSignMessage.run() wraps jade.signMessage; the anti-exfil
  tuple guard and recovery-byte failure still throw
  MalformedResponse.

Coldcard is file-based — no transport surface to wrap.

Tests cover the classifier (passthrough, statusCode 0x6985,
cancellation keywords, transport-shaped, non-Error throws) plus a
keystore-rejected and keystore-transport case for each of the four
direct keystores.
parse() previously assumed "second non-empty non-delimiter line
after BEGIN SIGNATURE" with no further validation. That works for
Coldcard's canonical output but silently produces garbage if the
format drifts (extra metadata line, wrapped address, comment). Two
guards:

1. Trim each candidate line before filtering, so a sig with
   surrounding whitespace still resolves.
2. Validate the resulting signature matches a base64 shape covering
   the BIP-137 65-byte wire form (80-120 base64 chars + optional
   `=` padding). A line that isn't base64 now throws
   MalformedResponse with a preview of the bad input instead of
   being handed downstream where verifyMessageSignature would
   silently return false.

Tests cover: non-base64 signature slot, too-short signature,
surrounding whitespace, and blank lines between marker and address.
- Ledger: add a v2-Bitcoin-app error-wrapping test mirroring the
  legacy-app coverage. Previously the v2 happy-path test was the
  only v2 case and it just asserted "mock returned what we told it
  to"; with this addition the v2 branch has at least one assertion
  that exercises real interaction-layer logic (the wrap in run()).
- BitBox: add a regtest test exercising the third branch of
  convertNetwork ('rbtc'). Mainnet ('btc') and testnet ('tbtc')
  were already covered; regtest is the path that gets hit in the
  docker e2e setup.
- Coordinator messageSigning fixture wiring: guard relativePath
  derivation with a startsWith check. If a future
  TEST_FIXTURES.multisigs entry has a bip32Path that doesn't sit
  under its branchPath, throw at suite-construction time with both
  paths in the message instead of silently deriving the wrong
  pubkey and showing a verification failure on-device.
Rewrite to match what actually lands:

- verifyMessageSignature takes {message, signature, expectedPubkey}
  directly (no Entry wrapper).
- MessageSigningErrorKind is 4 members, not 6 — UnsupportedAddressType
  and UnsupportedProtocol dropped along with the runtime legacy flag.
- New wrapSdkError export covers DeviceRejected/TransportError
  classification each per-keystore run() goes through.
- e.message is structured ([Kind/KEYSTORE] prefix); e.userMessage
  stays bare for UI surfaces.
The previous version was a design doc; the changelog only needs the
consumer-facing summary. Detail lives in the PR description.
The verifier, Entry type, and MessageSigningError taxonomy aren't
keystore-specific — they're the recovery side of any per-cosigner
message-signing flow. Pulling them into their own package now (at
@caravan/wallets's 1.0 inflection) so consumers can depend on the
verification surface without pulling the keystore drivers, and so
the future Sign-Message UI has a clean import target.

@caravan/wallets keeps the SignMessage factory, per-keystore
classes, and wrapSdkError (keystore SDK error classification stays
with the drivers). Signature fixtures live in
packages/messages/test-fixtures/signatures.json, regenerated via
`npm run generate-fixtures --workspace=@caravan/messages` when
TEST_FIXTURES changes.
private:true (won't be published) — matches the pattern in
@caravan/build-plugins. Moved to devDependencies in @caravan/wallets
so tsup inlines it into the published wallets bundle.

Drops the changeset for @caravan/messages since private packages
don't release.
The "expected" framing leaks the verifier's perspective into the
signing primitive. The signer side just has a pubkey; whether it's
"expected" is the caller's framing at verify time.

Renamed across @caravan/messages (Entry, verifyMessageSignature
args, fixtures) and @caravan/wallets (SignMessage factory,
per-keystore fields). Coordinator UI label "Expected pubkey:" stays
— that's the right framing for the user reading the test row.
- Strip internal-process language from production docstrings: no
  more sparrow / trezor-firmware issue links, SeedSigner PR
  pointers, or paragraph-length explanations of what protocols the
  class doesn't implement. Each per-keystore SignMessage class now
  says what it does in 2 lines.
- Replace try/catch error-assertion blocks with
  `rejects.toMatchObject({kind, keystore})` across ledger / trezor /
  jade / errors test files. Collapses each ~10-line block to 3-4
  lines.
- Extract mountLedgerApp helper (was 6 lines of spy plumbing
  repeated 5×) and a similar mountBitBoxSDK helper. Collapse the
  two BitBox coin-code tests + the mainnet happy-path into one
  parameterized it.each across all three networks.
Pre-computed BIP-137 and BIP-322 signatures now live as a
`signedMessages` field on each `TEST_FIXTURES.multisigs` entry,
signed at the entry's bip32Path with the open_source seed.
Signatures are deterministic so the values can be static literals —
no generator script needed.

Drop the runtime generator from @caravan/messages along with the
@scure/bip32, @scure/bip39, bitcoinjs-message, wif, and tsx
devDeps it was the only consumer of. Tests load TEST_FIXTURES
directly.

Also strip a leftover comment from the implementation notes.
@bucko13

bucko13 commented May 21, 2026

Copy link
Copy Markdown
Contributor Author

Running manual tests, Ledger:
Screenshot 2026-05-21 at 3 41 35 PM

@bucko13

bucko13 commented May 21, 2026

Copy link
Copy Markdown
Contributor Author

Jade passed (after some fixes):
Screenshot 2026-05-21 at 3 54 02 PM

@bucko13

bucko13 commented May 21, 2026

Copy link
Copy Markdown
Contributor Author

Trezor (mostly) passed. Note that P2SH signing fails, but it also fails on normal transaction signing due to a forbidden key path error.
Screenshot 2026-05-21 at 4 08 58 PM

BitBox firmware refuses sign_message at caravan's multisig-purpose
cosigner paths (BIP-45/48) — validates against BIP-49/84 only.
Add download-request-file button + .txt upload reader for the
messageSigning test category. Previously the test renderer fell
through to the xpub-upload branch (workflow=["parse"] only) and
showed the wrong prompt.
Test.runParse() unpacks Coldcard parse() output via Object.values()[0],
which assumes the legacy {pubkey: [sig]} shape and grabs bip32Path off
the SignMessageResult. Override runParse() in MessageSigningTest so
matches() receives the entry intact.
@bucko13

bucko13 commented May 21, 2026

Copy link
Copy Markdown
Contributor Author

And confirmed with coldcard (also needed a fix), this was MK3 only:

Screenshot 2026-05-21 at 4 34 54 PM

@bucko13
bucko13 marked this pull request as ready for review May 21, 2026 21:36
@bucko13
bucko13 requested a review from jpcummins May 21, 2026 21:38
Covers the messageSigning test category and Coldcard SD-card UX added
on this branch. Also unlocks the coordinator docker/e2e CI jobs gated
on caravan-coordinator appearing in a changeset.
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has been inactive for 30 days and has been marked as stale. It will be closed in 7 days if no further activity occurs. To keep this PR open, add the "long-lived" label or comment on it.

@github-actions github-actions Bot added the stale label Jun 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has been automatically closed due to inactivity for 7 days after being marked as stale. Feel free to reopen if needed!

@github-actions github-actions Bot closed this Jun 29, 2026
@bucko13 bucko13 reopened this Jul 12, 2026
@github-actions github-actions Bot removed the stale label Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has been inactive for 30 days and has been marked as stale. It will be closed in 7 days if no further activity occurs. To keep this PR open, add the "long-lived" label or comment on it.

@github-actions github-actions Bot added the stale label Aug 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has been automatically closed due to inactivity for 7 days after being marked as stale. Feel free to reopen if needed!

@github-actions github-actions Bot closed this Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant