Skip to content

[v10] Ground-up SDK rewrite with tree-shakeable architecture#842

Draft
gregnazario wants to merge 19 commits intomainfrom
greg/v10-sdk
Draft

[v10] Ground-up SDK rewrite with tree-shakeable architecture#842
gregnazario wants to merge 19 commits intomainfrom
greg/v10-sdk

Conversation

@gregnazario
Copy link
Collaborator

@gregnazario gregnazario commented Mar 7, 2026

Summary

  • Complete v10 rewrite of @aptos-labs/ts-sdk in the v10/ directory — ESM-only, function-first API, subpath exports, sideEffects: false
  • Layered architecture (BCS → Crypto → Core → Transactions → Account → Client → API) with no circular dependencies
  • Compat layer at @aptos-labs/ts-sdk/compat that preserves the v6 flat-method API (aptos.getAccountInfo(), aptos.transaction.build.simple()) for gradual migration
  • CI workflow (.github/workflows/run-v10-tests.yaml) with 7 parallel jobs: build+lint, unit tests, e2e tests, Bun runtime, Deno runtime, and Playwright browser tests

Key design changes from v6

v6 v10
Module format CJS + ESM bundle ESM-only (plain tsc)
HTTP client @aptos-labs/aptos-client Native fetch
API style Mixin-flattened class Function-first + namespaced Aptos class
Tree-shaking Not possible (barrel exports) Full support (subpath exports + sideEffects: false)
Dependencies 10 runtime deps 5 runtime deps
Target ES2020 ES2022, Node >= 22

Bundle size comparison

All sizes measured with esbuild, minified, crypto deps (@noble/*, @scure/*, poseidon-lite) externalized.

Full SDK import (new Aptos(...))

Scenario v6 v10 Reduction
Browser ESM (min) 234.5 KB 78.6 KB -66%
Browser ESM (gzip) 54.9 KB 17.9 KB -67%
Node ESM (min) 340.5 KB 78.6 KB -77%
Node ESM (gzip) 88.4 KB 17.9 KB -80%
Node CJS (min) 366.0 KB N/A (ESM-only)
Node CJS (gzip) 93.8 KB N/A (ESM-only)

Tree-shaken import (v10 only — getLedgerInfo + createConfig via subpath)

Scenario Size (min) Size (gzip)
Browser ESM 15.1 KB 4.5 KB
Node ESM 15.1 KB 4.5 KB

v6 cannot tree-shake — any import pulls in the full 234+ KB bundle.

Raw dist output

v6 v10
Total dist/ 6.8 MB 2.1 MB
ESM output 5.6 MB 1.2 MB
CJS output 1.2 MB N/A
Type declarations (included in above) 964 KB

Build time comparison

v6 (tsup) v10 (tsc) Speedup
Build time 12.8s 1.0s ~13x faster

Test coverage

  • 403 unit tests across BCS, crypto, core, transactions, account, client, API, and compat
  • 30 e2e tests (15 native API + 15 compat) against local testnet
  • Cross-runtime test projects for Bun, Deno, and browser (Playwright)

Test plan

  • CI workflow triggers on this PR (path filter: v10/**)
  • Build & lint job passes (pnpm build && pnpm check)
  • Unit tests pass (403 tests)
  • E2E tests pass against local testnet (30 tests)
  • Bun runtime test passes
  • Deno runtime test passes
  • Browser (Playwright) test passes

v10 is a complete rewrite of @aptos-labs/ts-sdk targeting modern
runtimes (Node 22+, Bun, Deno, browsers) with ESM-only output,
subpath exports, and function-first API design.

Key changes from v6:
- ESM-only (no CJS), plain tsc build (no bundler)
- Function-first API: each operation is a standalone importable function
- Namespaced Aptos class via composition (not runtime mixins)
- Native fetch (drops @aptos-labs/aptos-client dep)
- Native btoa/atob (drops js-base64 dep)
- Subpath exports: @aptos-labs/ts-sdk/bcs, /crypto, /core, etc.
- sideEffects: false for full tree-shaking
- Compat layer at @aptos-labs/ts-sdk/compat for gradual migration

Architecture (layered, no circular deps):
  L0: bcs, hex
  L1: crypto (ed25519, secp256k1, secp256r1, multi-key, keyless)
  L2: core (AccountAddress, TypeTag, Network, errors)
  L3: transactions (builder, payloads, authenticators)
  L4: account (8 account types + factory)
  L5: client (native fetch HTTP client)
  L6: api (standalone functions + Aptos facade class)

Testing: 403 unit tests + 28 e2e tests (13 native + 15 compat)
CI: GitHub Actions workflow with unit, e2e, Bun, Deno, and Playwright jobs
@gregnazario gregnazario requested a review from a team as a code owner March 7, 2026 23:20
…types

- Auto-fix import ordering across 18 files
- Remove unused imports (APTOS_COIN, GasEstimation, TransactionAuthenticatorEd25519, createAuthKey, AptosApiType, createConfig)
- Replace banned `{}` type with `Record<string, unknown>` in MoveResource and pagination generics
- Prefix unused `coinType` param with underscore
@gregnazario gregnazario marked this pull request as draft March 7, 2026 23:30
Adds a thin .cjs wrapper at ./compat that enables require() for
v6 CJS users migrating to v10. Uses sync require(esm) on Node
22.12+ and falls back to exporting the import() promise on older
Node 22.x. No dual-package hazard since both paths load the same
ESM module.
- Replace `any` with `unknown`, proper types, and targeted casts across
  all source and test files (46 warnings → 0)
- Introduce lazy constructor interfaces (LazyPublicKeyClass,
  LazyAccountAddressClass) for forward-declared types to avoid circular
  deps without resorting to `any`
- Restore src/compat/index.cjs and add `require` export condition +
  build copy step
- Update Aptos constructor to accept AptosSettings | AptosConfig
- Includes noble-curves/hashes v2 migration and biome formatter fixes
- Broaden vitest include to tests/**/*.test.ts so test:e2e filter finds
  e2e test files (was only matching tests/unit/)
- Remove pnpm-workspace.yaml (moved ignoredBuiltDependencies into
  package.json) — its presence made pnpm treat v10 as a workspace root,
  causing examples/web-test pnpm install to skip local devDependencies
- Add deno install step to CI before deno task test so node_modules are
  resolved for manual nodeModulesDir mode
Add pluggable HTTP client interface (Client/ClientRequest/ClientResponse)
threaded through all API call sites, with tests for custom client usage.

Audit fixes (round 1):
- Narrow waitForTransaction polling to only suppress 404 errors
- Add URL scheme validation and credential sanitization in errors
- Add private key clear() lifecycle for Secp256r1PrivateKey
- Change network maps to Partial<Record<Network, string>> for type safety
- Remove silently-ignored coinType param from transferCoinTransaction
- Fix typos in error messages (authentiate, asyncronous)
- Mark AccountAddress constants as static readonly
- Use instanceof ParsingError in isValid() instead of unsafe cast
- Constant-time comparison for Hex.equals() and AccountAddress.equals()
- Remove redundant type casts in Ed25519Account
- Change getSigningMessage from async to sync (no await needed)
- Fix no-op ternary in compat Aptos constructor
- Change default resource limit from 999 to 1000

Audit fixes (round 2):
- Fix secp256r1 signature malleability: add lowS:true to sign/verify
- Fix p256.Point.CURVE() wrong API → use Point.Fn.ORDER for curve order
- Fix double-hash in AbstractedAccount.signTransactionWithAuthenticator
- Guard poseidonHash against empty input arrays
- Add max iteration guard (1000 pages) to paginateWithCursor
- Add MoveVector.MAX_DESERIALIZE_LENGTH (2^20) to prevent DoS
- Add clearSensitiveData() to AbstractKeylessAccount for pepper zeroing
- Document JWT storage security implications in AbstractKeylessAccount
…stness

- waitForTransaction: swallow only 404/429, re-throw other errors
- buildSimpleTransaction: always fetch ledgerInfo for chain ID
- AbstractedAccount.signTransaction: wrap with AA envelope
- AbstractedAccount.signWithAuthenticator: remove extra SHA3-256
- MultiEd25519Account: use sorted signerIndices for bitmap
- Secp256r1PrivateKey: extend Serializable, add serialize/deserialize
- V1 abstraction authenticator: use serializeBytes for composability
- bigIntToBytesLE: add overflow and negative value checks
- JWT aud: improve error message for array aud values
- Secp256k1/Secp256r1 public keys: validate uncompressed curve points
- EphemeralKeyPair.serialize: check cleared state
- MultiEd25519PublicKey.deserialize: add alignment validation
- AbstractKeylessAccount.verifySignature: document unsupported
- Poseidon error messages: fix ${bytes} → ${bytes.length}
…ness

- Add convertSigningMessage to Secp256r1 sign/verify for consistency with Ed25519/Secp256k1
- Use generateSigningMessage in Groth16ProofAndStatement.hash() for proper domain separation
- Move serializer pool reset to releaseSerializer for immediate buffer zeroing
- Zero intermediate HD key derivation material (digest, CKDPriv data, parent keys)
- Type-check JWT header kid field is a string, not just defined
- Add JWT claims length validation (iss, aud, uidVal) against on-chain limits
- Add bounds checks to MultiEd25519Signature.deserialize
- Add JWT header length check in KeylessSignature.deserialize
- Compute RSA modulus size dynamically in MoveJWK.toScalar for >2048-bit keys
- Wrap atob in try-catch for base64url error handling
- Truncate raw input in deserialization error messages to prevent log flooding
- Add nonce documentation explaining why it is not zeroed on clear
- Fix createBitmap bounds checking (bit value vs loop index)
- Fix MoveVector empty non-U8 vector serialization with _isU8 discriminant
- Fix AIP-80 prefix parsing to use slice instead of split
- Fix ULEB128 decoder to reject continuation bit on terminal byte
- Add MAX_KEYS validation to MultiKey constructor
…ormance

Security:
- S2: EphemeralKeyPair uses === undefined for expiryDateSecs (0 is valid)
- S3: Secp256k1 HD derivation zeros HDKey internals after extracting key
- S4: parseJwtHeader size guard + returns only {kid}
- S5: MoveJWK.toScalar RSA modulus size limit (max 512 bytes)
- S6: MultiEd25519 deserialize uses .slice() instead of .subarray()
- S7: Deserialization utils rethrow TypeError/RangeError
- S9: AccountAddress constructor defensive copy via .slice()
- S10: G1Bytes/G2Bytes readonly data + defensive copy
- S11: WebAuthnSignature fields made readonly

Correctness:
- C1: Serialized.toMoveVector uses this.value directly (no re-serialize)
- C2: MultiKey.verifySignature returns false instead of throwing
- P8 fix: convertSigningMessage regex accepts hex with or without 0x prefix

Performance:
- P1: bcsToBytes() uses serializer pool
- S1: serializeAsBytes uses toUint8Array() (safe with pool zeroing)
- P3: serializeU8/serializeBool write directly to buffer
- P4: ULEB128 encoder writes directly to buffer
- P5: validateNumberInRange fast path for plain numbers
- P6: generateSigningMessage caches domain separator hashes
- P7: isCanonicalEd25519Signature reads at offset (no slice)
- P9: ULEB128 decoder uses plain number arithmetic
- P10: AuthenticationKey.fromSchemeAndBytes avoids spread syntax
- P11: MoveVector uses module-level TextEncoder
- P12: MoveString uses module-level TextEncoder
- P13: CKDPriv uses pre-allocated buffer + DataView
- P14: MoveVector.U8 avoids intermediate Array.from
- P15: Groth16Zkp.deserialize reads .data directly
- P16: poseidon.ts hoists BigInt constants
- P17: hexToAsciiString reuses module-level TextDecoder
- P18: Secp256r1Signature reuses Hex when no normalization needed
…ormance

Correctness:
- MoveVector.deserialize passes isU8 flag for correct script serialization
- MultiEd25519PublicKey.verifySignature returns false instead of throwing
- Remove redundant double Hex.fromHexInput in authenticator.ts

Security:
- Add JWT size limits (8192 bytes) to decodeJwtPayload/decodeJwtHeader
- Ed25519PrivateKey.fromDerivationPathInner zeros key on constructor failure
- Bound domainSeparatorCache to 64 entries with FIFO eviction

Performance:
- Hoist TextEncoder to module level in poseidon.ts, keyless.ts, abstract-keyless-account.ts
- Replace BigInt() calls with cached constants in serializer, deserializer, move-primitives
- AccountAddress.isSpecial() uses loop instead of .slice() allocation
- AbstractMultiKey.getIndex uses byte comparison instead of toString()
…rformance

Correctness:
- MoveOption.deserialize rejects vectors with >1 element (BCS contract)
- KeylessAccount.serialize guards against cleared sensitive data

Security:
- Bound HD key derivation path segments to [0, 2^31) to prevent silent
  truncation in DataView.setUint32
- Add safe-integer guards for bigint→number conversions on timestamp
  fields (expiryDateSecs, expHorizonSecs) in keyless deserialization

Performance:
- serializeAsBytes uses toUint8ArrayView() to avoid redundant copy
- validateNumberInRange skips BigInt() coercion when bounds already bigint
- padAndPackBytesWithLen uses push() instead of concat()
- G2Bytes.toProjectivePoint uses subarray+reverse instead of slice+reverse
…erformance

- Add recursion depth limit (128) to TypeTag.deserialize() to prevent
  stack overflow from deeply nested vector/struct payloads
- Validate MultiKeySignature bitmap is exactly 4 bytes after deserialization
- Add size limits to WebAuthnSignature authenticatorData (2 KB) and
  clientDataJSON (4 KB) fields during deserialization
- Pre-compute byte-to-bigint lookup table in poseidon.ts to avoid
  per-byte BigInt allocations in bytesToBigIntLE
Security:
- StructTag.deserialize passes depth+1 to inner TypeTag deserialization
  and caps typeArgs count at 32
- WebAuthnSignature.deserialize bounds signature field at 128 bytes
- MultiSigTransactionPayload.deserialize validates variant index
- Multi-agent/fee-payer secondary signer vectors capped at 255

Performance:
- serializeU64/U128/U256 call BigInt(value) once instead of 2-4x
- Deserializer uses constructor-built DataView singleton instead of
  per-call allocations for U16/U32/I8/I16/I32
- TypeTagVector.u8(), aptosCoinStructTag(), stringStructTag() return
  cached singletons
- AccountAddress.toStringWithoutPrefix fast-paths special addresses
  to skip full hex encoding
Comprehensive README covering installation, quick start (ESM/CJS),
architecture overview, full API reference, and custom HTTP client docs.
Migration guide with compat layer instructions, breaking changes with
before/after examples, and complete v6→v10 method mapping.
Adds a standalone example project under v10/examples/simple-transfer/
that demonstrates the core Aptos transaction flow: generate accounts,
fund via faucet, build a transfer, sign & submit, wait for confirmation,
and verify the recipient's balance.

Includes:
- src/main.ts: runnable script showing the full flow on devnet
- tests/unit/transfer.test.ts: mocked unit test covering the entire flow
- tests/e2e/transfer.e2e.test.ts: real devnet test (skipped unless APTOS_E2E=1)
Unit tests that mock all HTTP should use LOCAL to make intent clear.
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