Skip to content

refactor: sign with the official transaction client - #122

Merged
yeboster merged 1 commit into
mainfrom
refactor/official-transaction-client
Aug 29, 2026
Merged

refactor: sign with the official transaction client#122
yeboster merged 1 commit into
mainfrom
refactor/official-transaction-client

Conversation

@yeboster

Copy link
Copy Markdown
Contributor

Step 2 and 3 of PLAN-drop-unmaintained-partisia-deps.md. #120 did step 1.

Why

partisia-blockchain-applications-crypto was last published 2024-10-04 and is unmaintained. It carries 11 advisories, 5 critical, pins bip32@=2.0.6 and bip39@=3.1.0 with = so no patched version can float in, and pulls zxcvbn (3.4 MB installed) — a password-strength dictionary — into a blockchain SDK.

The official replacement, @partisiablockchain/blockchain-api-transaction-client, was already installed as a dependency of @partisiablockchain/abi-client. This makes it a direct dependency and hands it the signing.

What

SignedTransaction.create serializes, hashes and signs in one call. The four createTransactionFrom* functions — each repeating serialize → digest → sign → concatenate → base64 → broadcast — collapse into one createTransaction plus four small SenderAuthentication implementations in the new transactions/authentication.ts:

strategy implementation
privateKey SenderAuthenticationKeyPair.fromString — provided by the official client
Ledger wraps the existing PartisiaLedgerClient
MetaMask wraps the existing snap wallet_invokeSnap call
partisiaSdk wraps PartisiaSdk.signMessage

serializeTransaction is deleted from transactions/helper.ts, which keeps builderToBytesBe, getChainId and the result poller. src/transactions/index.ts goes from 198 lines of four near-identical pipelines to one function.

The public API does not move: setSigningStrategy is unchanged, and ContractRepository.createTransaction picks the backend behind the same dynamic import as before.

The bytes are identical — verified, not assumed

Checked against the old package for a fixed key, nonce, gas, payload and chain id:

equal
account address from private key
serialized transaction (nonce, valid-to, gas as BE i64s; contract address; length-prefixed payload)
signing digest (SHA-256 over those bytes + length-prefixed chain id)
signature ❌ — see below

The one difference: the old package signed with elliptic's canonical: true, the official client uses the default. That produces the low-s and high-s forms of the same signature — same r, and s_old + s_new == n. Both are valid ECDSA over the same digest and recover the same public key, since the 65-byte encoding carries the recovery parameter. The full testnet suite (≈40 real transactions: registrations, transfers, renewals, record mints and updates) passes, so the chain accepts them.

Behaviour changes, deliberate

  • The Partisia wallet no longer broadcasts. It signs with dontBroadcast: true and the SDK broadcasts through the reader node, so every strategy reaches the chain the same way and its result is polled the same way.
  • Nonce-collision retries are limited to non-interactive signers. Re-signing after a rejected broadcast is free for a private key; for a Ledger or a wallet it would silently prompt the user a second and third time.

Measured

esbuild --bundle --minify --format=esm --splitting --platform=node, against main (with #120 in):

bytes
all chunks on main 915,298
all chunks here 543,045
−372,253 (−40.7%)
bytes
entry chunk on main 345,347
entry chunk here 247,853
−97,494 (−28.2%)

The 343,654-byte wallet-crypto chunk — bip39's wordlists for every supported language — is gone entirely.

Production advisory count does not change (3 low, all elliptic, already the state after #115's resolutions). The dependency graph stays at 107 packages too, because partisia-blockchain-applications-sdk still depends on the crypto package. What changes is structural: no SDK code path imports the abandoned package any more, so nothing it pins can break a build or ship in a bundle.

What stays

partisia-blockchain-applications-sdk — the browser wallet connector, no official replacement (@partisiablockchain/snap covers the MetaMask path the SDK already calls directly). It is used as a type only: the connected client is passed in by the consumer, so the SDK never imports it or its crypto subtree at runtime.

Note for the app-side migration

PLAN-drop-unmaintained-partisia-deps.md suggests importing CryptoUtils from the package root. It is not exported there — @partisiablockchain/blockchain-api-transaction-client re-exports the OpenAPI runtime, BlockchainTransactionClient, SignedTransaction, SenderAuthenticationKeyPair and the types, but CryptoUtils has to come from @partisiablockchain/blockchain-api-transaction-client/target/main/CryptoUtils. Its keyPairToAccountAddress(privateKeyToKeypair(pk)) does produce the same address as the old privateKeyToAccountAddress — verified above.

Test status

npx tsc --noEmit clean, eslint clean, yarn build clean, npx jest -i21 suites / 269 tests pass against the live testnet.

https://claude.ai/code/session_01GceWCwGXu66D1xEBDxZWBb

`partisia-blockchain-applications-crypto` was last published 2024-10-04 and is
no longer maintained. It carried 11 advisories, 5 of them critical, pinned
`bip32` and `bip39` to exact versions that cannot be patched, and pulled in
zxcvbn -- a 3.4 MB password-strength dictionary -- for a blockchain SDK.

The official replacement, `@partisiablockchain/blockchain-api-transaction-client`,
was already installed as a dependency of `@partisiablockchain/abi-client`. It is
now a direct dependency and does the signing.

`SignedTransaction.create` serializes, hashes and signs in one call, so the four
`createTransactionFrom*` functions -- each repeating serialize, digest, sign,
concatenate, base64, broadcast -- collapse into a single `createTransaction`
plus four small `SenderAuthentication` implementations in
`transactions/authentication.ts`:

  privateKey    SenderAuthenticationKeyPair.fromString (provided)
  Ledger        wraps the existing PartisiaLedgerClient
  MetaMask      wraps the existing snap wallet_invokeSnap call
  partisiaSdk   wraps PartisiaSdk.signMessage

`serializeTransaction` in `transactions/helper.ts` is gone; the module keeps
`builderToBytesBe`, `getChainId` and the result poller.

The bytes are identical. Verified against the old package for a fixed key,
nonce, gas and payload: same account address, same serialized transaction
(nonce, valid-to and gas as big-endian i64s, then the contract address and the
length-prefixed payload) and same signing digest (SHA-256 over those bytes plus
the length-prefixed chain id).

One deliberate difference: the old package signed with elliptic's
`canonical: true`, the official client uses the default, so the low-s and high-s
forms of the same signature are produced. Both are valid ECDSA over the same
digest and recover the same public key -- the 65-byte encoding carries the
recovery parameter -- and the full testnet suite confirms the chain accepts
them.

Two behavioural notes:

- The Partisia wallet now signs with `dontBroadcast: true` and the SDK
  broadcasts, so every strategy goes to the chain the same way and its result
  is polled the same way.
- Broadcast retries on a spent nonce are limited to non-interactive signers.
  Retrying a Ledger or wallet signature would silently prompt the user again.

Bundled with esbuild (--bundle --minify --format=esm --splitting), total bytes
across all chunks 915,298 -> 543,045 (-372,253, -40.7%); the entry chunk
345,347 -> 247,853 (-28.2%). The 343 KB wallet-crypto chunk is gone.

`partisia-blockchain-applications-sdk` stays: it is the browser wallet
connector and has no official replacement. It is used as a type only -- the
connected client is passed in by the consumer -- so the SDK never imports it or
its crypto subtree at runtime.

21 suites / 269 tests pass against live testnet.
@github-actions

Copy link
Copy Markdown

Total Coverage: 91.88%

Coverage Report
File Branch Funcs Lines Uncovered Lines
src
   actions.ts 100% 100% 100%
   index.ts 100% 100% 100%
   interface.ts 100% 100% 100%
   meta-names-sdk.ts 75% 75% 95.65% 22, 44, 45
   partisia-name-system.ts 58% 87.50% 84.07% 106, 114, 12, 139, 152, 160, 164, 169, 170, 171, 178, 180, 188, 189, 19, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 22, 220, 221, 222, 29, 33, 42, 49, 54, 55, 7, 78, 82
   version.ts 100% 100% 100%
src/actions
   domain.ts 64.29% 100% 100% 35, 44, 56, 71, 74
   record.ts 77.78% 100% 100% 14, 48
src/models
   domain.ts 100% 83.33% 95.95% 45, 46, 47
   index.ts 100% 100% 100%
src/models/helpers
   domain.ts 100% 100% 100%
src/providers
   config.ts 66.67% 100% 98.11% 40, 49, 50
   index.ts 100% 100% 100%
   secrets.ts 45.45% 62.50% 78.57% 19, 23, 24, 25, 26, 27, 29, 30, 44, 50, 51, 52, 53, 56, 57, 58, 59, 62, 63, 64, 65
src/providers/config
   mainnet.ts 100% 100% 100%
   testnet.ts 100% 100% 100%
src/repositories
   contract-repository.ts 76.92% 100% 98.28% 101, 112, 135, 136, 138, 139, 141, 142, 144, 145, 154, 170, 192, 208, 32, 44, 45, 55, 61
   domain-repository.ts 60% 100% 100% 102, 113, 117, 118, 133, 142, 150, 176, 220, 225, 240, 256, 262, 269, 34, 53, 54, 96, 98, 99
   index.ts 100% 75% 100%
   record-repository.ts 71.43% 100% 100% 24, 34, 50, 60
src/repositories/contracts
   meta-names-contract-repository.ts 84.62% 88.89% 86.14% 57, 58, 59, 60, 61, 62, 63, 64, 78, 79, 80, 81, 82, 83, 84, 94
src/repositories/helpers
   avl-client.ts 80% 77.78% 61.62% 16, 22, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 95
   client.ts 92.86% 100% 100% 64
   contract.ts 75% 100% 92.45% 41, 42, 43, 44, 45, 50
   sharded-client.ts 57.69% 100% 88.24% 109, 109, 110, 111, 112, 113, 114, 115, 116, 117, 121, 138, 155, 156, 157, 158, 160, 161, 162, 163, 164, 176, 176, 177, 178, 179, 180, 181, 59, 67, 81, 84, 92
src/transactions
   authentication.ts 100% 25% 40% 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 93, 94, 95, 96, 97, 98, 99
   helper.ts 76.92% 100% 92.06% 31, 32, 33, 34, 35, 55, 56, 9
   index.ts 33.33% 20% 93.67% 52, 56, 73, 74, 75, 77, 77, 78, 79
src/validators
   base-validator.ts 100% 100% 100%
   domain-validator.ts 87.50% 100% 100% 29, 30, 31
   index.ts 100% 100% 100%
   record-validator.ts 100% 100% 100%
src/validators/idna
   index.ts 81.54% 100% 95.93% 108, 110, 110, 111, 118, 119, 120, 121, 122, 123, 124, 125, 136, 138, 139, 144, 151, 155, 168, 210, 46
   table.ts 100% 100% 100%
src/validators/records
   default-validator.ts 100% 100% 100%
   discord-validator.ts 100% 100% 100%
   email-validator.ts 100% 100% 100%
   main-validator.ts 100% 100% 100%
   price-validator.ts 100% 100% 100%
   regex-validator.ts 100% 100% 100%
   twitter-validator.ts 100% 100% 100%
   uri-validator.ts 100% 100% 100%
   wallet-validator.ts 100% 100% 100%
test
test/helpers
   config.ts 0% 100% 100% 7
   helper.ts 100% 60% 68% 25, 26, 27, 28, 29, 30, 31, 32, 35, 36, 37, 38, 39, 40, 41, 42
   index.ts 100% 100% 100%

@yeboster
yeboster merged commit 6dc4535 into main Aug 29, 2026
8 checks passed
@yeboster
yeboster deleted the refactor/official-transaction-client branch August 29, 2026 10:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant