Skip to content

chore: regenerate SDK from current public API schema - #20

Merged
figitaki merged 8 commits into
mainfrom
carey/codegen-bump-2026-10
Oct 18, 2026
Merged

figitaki merged 8 commits into
mainfrom
carey/codegen-bump-2026-10

Conversation

@figitaki

@figitaki figitaki commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR regenerates the Python SDK from the current Turnkey public API schema.

  • The schema comes from tkhq/sdk at commit 7b456b03513cef96dbc17dc6ed36cc55b9212f0e, file packages/sdk-types/src/__inputs__/public_api.swagger.json. That file last changed in 5e732215f1271d05a495e6c45ed77cd689383425. The repo has no convention for recording the source sha in the schema, so this PR does not add one.
  • Activity types: 121 before, 173 after.
  • API endpoints and client methods: 114 before, 189 after. Each method also has a stamp_ variant. No method or type was removed.
  • New method families: send transaction V2, swaps, earn, secrets, transaction history. The schema also adds TVC, MFA policies, webhooks, IP allowlist, session profiles, and Spark endpoints.

Commits

  1. chore: update public API schema from tkhq/sdk. This commit only replaces the schema file.
  2. fix(codegen): handle the current public API schema in the HTTP generator. This is a small fix in codegen/. Without it, make generate fails on the new schema. See "Generator changes" below.
  3. chore: regenerate SDK types and HTTP client. This is the output of make generate, with one test update.
  4. chore: add changeset for the codegen bump. This adds a minor changeset for http and sdk-types.

How it was generated

  • Python 3.12, in a project virtualenv.
  • make install
  • make generate (this runs generate-types and then generate-http)
  • make format. This also reformats Python code blocks in three README files. That happens on main too, because newer ruff formats Markdown. I left those README changes out of this PR.

Generator changes

On the new schema, make generate-http failed with KeyError: 'ValidateTvcImageResult'. Fixing only the crash would still leave a client that sends incorrect activity types. The fix is in codegen/utils.py and codegen/http/generate_http.py only. Generated files have no hand edits.

  • validateTvcImage is a query endpoint, but its name does not start with get, list, or test. The generator classified it as an activity. Names that start with validate are now queries. The TS SDK codegen does the same.
  • The HTTP generator built the activity type from the method name. The types generator builds the request body from the schema's latest intent. For eth_send_transaction, sol_send_transaction, create_swap_quote, execute_swap, and create_oauth_providers, the client would have sent the old unversioned type with the new intent body. For eth_undelegate7702, it would have sent ACTIVITY_TYPE_ETH_UNDELEGATE7702, which does not exist. The generator now reads the type from the request definition, as the TS SDK codegen does. The pins in VERSIONED_ACTIVITY_TYPES still take precedence.
  • The HTTP generator always unpacked the latest result key, even for a pinned activity. create_sub_organization is pinned to V7, but the schema now has a V8 result. Without this fix, a completed V7 activity would not unpack its result. The generator now uses the pinned result type, as the types generator already does.

I generated again from the old schema with the new codegen. The output is identical to main.

Activity versions sent

Method Activity type sent
eth_send_transaction ACTIVITY_TYPE_ETH_SEND_TRANSACTION_V2
sol_send_transaction ACTIVITY_TYPE_SOL_SEND_TRANSACTION_V2
create_swap_quote ACTIVITY_TYPE_CREATE_SWAP_QUOTE_V3
execute_swap ACTIVITY_TYPE_EXECUTE_SWAP_V3
earn_deposit ACTIVITY_TYPE_EARN_DEPOSIT
earn_withdraw ACTIVITY_TYPE_EARN_WITHDRAW
init_import_secrets ACTIVITY_TYPE_INIT_IMPORT_SECRETS
export_secrets ACTIVITY_TYPE_EXPORT_SECRETS
create_sub_organization ACTIVITY_TYPE_CREATE_SUB_ORGANIZATION_V7 (pinned, no change)
create_users ACTIVITY_TYPE_CREATE_USERS_V3 (pinned, no change)

These methods are queries: get_swap_status, list_earn_enabled_vaults, list_earn_positions, list_eth_transaction_history, list_sol_transaction_history, validate_tvc_image.

Testing

  • make typecheck: passes on main and on this branch.
  • make format-check: fails on main and on this branch for the same three README files. The cause is newer ruff, which formats Markdown code blocks. All Python files pass.
  • CI "Build & Test" fails at the format check step for this reason. The job stops there, so CI does not run typecheck. I ran typecheck locally (see above).
  • make test, offline:
    • packages/sdk-types/tests/test_pydantic_aliases.py passes. The test builds EthSendTransactionBody. I changed it to use the V2 shape (calls=[{"to": ...}] replaces to=...). It still checks the from alias.
    • The 7 tests in packages/http/tests need live credentials (tests/.env, see tests/.env.example). I did not run them against the API. Without credentials they fail the same way on main and on this branch. They fail, they do not skip. The tests are test_create_api_keys, test_organization_id_override, test_stamp_create_api_keys_send_signed_request, test_get_whoami, test_get_organization, test_get_users, and test_get_wallets.
    • conftest.py imports dotenv, but python-dotenv is not in any dev dependency list. I installed it by hand.
  • Offline smoke check, with no network and a throwaway P-256 key:
    • I built a TurnkeyClient and confirmed that the 13 new methods above exist with typed bodies.
    • I used the stamp_* methods to read the activity type each method sends (see the table).
    • With requests.post mocked, the X-Client-Version header is still turnkey-python/0.1.0 from version.py.
    • With requests.post mocked, a completed V2 send transaction and a completed V7 sub-organization both unpack their result fields.

Breaking changes for existing callers

  • EthSendTransactionBody now uses the V2 intent. to, value, and data are gone. calls: List[v1EthCallParams] is required.
  • SolSendTransactionBody now uses the V2 intent. signWith is gone. signWiths: List[str] is required.
  • CreateOauthProvidersBody.oauthProviders is now List[v1OauthProviderParamsV2], and the method sends V2.
  • Oauth2AuthenticateBody.nonce is now required.
  • v1User.mfaPolicies is now a required response field. Responses that do not include it fail validation.
  • v1FiatOnRampBlockchainNetwork no longer has FIAT_ON_RAMP_BLOCKCHAIN_NETWORK_BITCOIN.
  • v1CreateTvcDeploymentIntent drops pivotBindAddresses and requires healthCheckType, healthCheckPort, and publicIngressPort.

The packages are still 0.x, so the changeset uses minor. Reviewers can change this to major if they prefer.

Follow-ups

Tracked outside this PR:

🤖 Generated with Claude Code

figitaki and others added 5 commits October 16, 2026 15:36
Copy public_api.swagger.json from tkhq/sdk at commit
7b456b03513cef96dbc17dc6ed36cc55b9212f0e
(packages/sdk-types/src/__inputs__/public_api.swagger.json, last changed
in 5e732215f1271d05a495e6c45ed77cd689383425).

The schema now has 173 activity types (was 121) and 189 endpoints
(was 114).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The new schema breaks the HTTP generator in three ways:

- validateTvcImage is a query (/public/v1/query/), but its name does
  not start with get, list or test. The generator treated it as an
  activity and crashed with KeyError: 'ValidateTvcImageResult'. Names
  that start with "validate" are now queries. The TS SDK codegen does
  the same.
- The client built the activity type from the method name. For
  eth_send_transaction, sol_send_transaction, create_swap_quote,
  execute_swap and create_oauth_providers it sent the unversioned type
  with a newer intent body. For eth_undelegate_7702 it sent
  ACTIVITY_TYPE_ETH_UNDELEGATE7702, which does not exist. The generator
  now reads the type from the request definition, like the TS SDK
  codegen. Pins in VERSIONED_ACTIVITY_TYPES still take precedence.
- The client always used the latest result key, even when the activity
  type is pinned. create_sub_organization is pinned to V7, but the
  schema now has a V8 result, so V7 results were not unpacked. The
  generator now uses the pinned result type, as the types generator
  already does.

Generating from the previous schema gives the same output as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Output of `make generate` against the updated public API schema.
No hand edits to generated files.

- 114 to 189 API methods (each with a stamp_ variant). Nothing removed.
- New: send transaction V2, swaps, earn, secrets, transaction history,
  TVC, MFA policies, webhooks, IP allowlist, session profiles, Spark.
- eth_send_transaction and sol_send_transaction now use the V2 intents.

test_pydantic_aliases builds EthSendTransactionBody. It now uses the V2
shape (`calls` replaces `to`). The test still checks the `from` alias.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ruff 0.17 formats Python code blocks inside Markdown, so make format-check
fails on three READMEs that have not changed since March. This fails on
main too. Excluding *.md keeps the check on code only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Documented public API breaking changes and limited integration validation warrant human compatibility review.

No open issues 🎉

What changed in this PR

Updates code generation to support the current Turnkey public API schema and newer SDK request shapes.

Changes:

  • Reads activity types from the schema, preserves pinned results, and recognizes validation queries.
  • Updates alias tests for Ethereum transaction V2.
  • Excludes Markdown from Ruff and adds a minor-release changeset.
File Description
pyproject.toml Excludes Markdown from Ruff formatting.
packages/​sdk-types/​tests/​test_pydantic_aliases.py Updates alias tests to use V2 transaction calls.
codegen/​utils.py Classifies validation methods as queries.
codegen/​http/​generate_http.py Resolves schema activity types and pinned result keys.
.changeset/​codegen-bump-2026-10.yml Records minor releases for HTTP and SDK types.

Review effort: Balanced

@emostov

emostov commented Oct 17, 2026

Copy link
Copy Markdown

Adversarial review

I approached this as "assume the regen is wrong and try to prove it." Most of the attack failed; details at the bottom. Two things survived, one of which is a regression in an existing public code path.

Setup used for everything below: fresh clone at e4455e0, fresh venv (Python 3.14, pydantic 2.14.1, ruff 0.17.1), make install, then make generate. All repros are offline with requests.post mocked.


  1. [demonstrated] send_signed_request with a typed response now fails on a completed eth_send_transaction, sol_send_transaction, and create_oauth_providers. This is a regression for existing callers.

    On main these three unpacked ethSendTransactionResult / solSendTransactionResult / createOauthProvidersResult. After this PR they unpack ...ResultV2. The typed methods pass the exact key to _activity, so they work. But send_signed_request finds the result by scanning dir(result) for attr_name.endswith('Result') (client.py, generated from generate_http.py send_signed_request). ethSendTransactionResultV2 ends in ResultV2, so nothing is flattened.

    Given: sr = client.stamp_eth_send_transaction(EthSendTransactionBody(...)), server returns a COMPLETED activity with result.ethSendTransactionResultV2 = {sendTransactionStatusId: "sts-1"}.

    • client.send_signed_request(sr, EthSendTransactionResponse) → ValidationError: 1 validation error for EthSendTransactionResponse / sendTransactionStatusId / Field required
    • client.send_signed_request(sr) (untyped) → returns the raw payload with no result fields flattened; on main the same call returned {"sendTransactionStatusId": ...} merged in.
    • Control: stamp_sign_transaction + send_signed_request(sr, SignTransactionResponse) still works, because its key is the unversioned signTransactionResult.

    I confirmed with a script over both client.py files that these three (plus their stamp_ twins) are the only pre-existing methods whose sent type or result key changed, so the blast radius is exactly them. The PR's follow-up note describes the ...V7 key gap as pre-existing, and it is, but this PR moves three methods that currently work into the broken set. #21 is stacked on this PR; if it fixes the generic path's key matching it should land in the same release, otherwise this should not ship alone.

    Cheapest fix: match re.search(r"Result(V\d+)?$", attr_name), or better, drop the dir() scan and use result.model_fields_set / model_dump(exclude_none=True) and take the single populated key. Even better long-term: put the result key on SignedRequest at stamp time so the generic path doesn't have to guess.

  2. [demonstrated, pre-existing, made far more reachable] Typed activity methods raise ValidationError instead of returning the activity when it is not COMPLETED, and the new activity families make that the common case.

    _activity falls through to response_type(activity=activity) for FAILED / REJECTED / CONSENSUS_NEEDED / still-PENDING. Response types declare result fields as required (generate_types.py, is_all_optional=False for activity responses), so:

    • eth_send_transaction with server status ACTIVITY_STATUS_FAILED → ValidationError: EthSendTransactionResponse / sendTransactionStatusId / Field required. The caller never sees activity.failure.
    • Same activity still PENDING after max_polling_retries → same ValidationError.

    Default polling budget is 3 × 1000ms. eth_send_transaction V2 and sol_send_transaction V2 broadcast, swaps route through external providers, earn deposits/withdrawals and TVC deployments are slow. With a 3-second budget the PENDING branch will be hit routinely, and the error the user sees is a pydantic missing-field error that says nothing about the activity. Tracked in #21; I'd treat shipping this regen without it as shipping a client whose headline new methods fail with a misleading error under normal latency. At minimum, bump the default polling budget for this release.

  3. [suspected] Pin drift relative to the TypeScript SDK is now silent.

    codegen/constants.py pins 16 activities. The TS codegen (packages/sdk-server/scripts/codegen.js) additionally pins CREATE_OAUTH_PROVIDERS → V2, ETH_SEND_TRANSACTION → V2, SOL_SEND_TRANSACTION → V2, CREATE_SWAP_QUOTE → V2, EXECUTE_SWAP → V3, and has moved CREATE_SUB_ORGANIZATION → V8, CREATE_USERS → V4. With the new "take the version from the request definition" logic, anything unpinned in Python will silently move to whatever the next schema says. The three methods this PR deliberately moves to V2 are exactly the ones a future regen could move again without anyone noticing in the diff (the type string change is buried in a 5000-line generated file). Suggest: pin the three you moved, and add a generator-time assertion that any activity whose sent type differs from the previous client.py is listed in the PR. #23 covers V8/V4.

    Note that CREATE_SWAP_QUOTE is a real divergence from TS today: Python sends V3, TS sends V2. The result key Python unpacks (createSwapQuoteResultV2) is correct for V3 (checked against the backend runner and the public docs), so this is a product decision, not a bug, but it should be a conscious one.

  4. [suspected] Changeset is minor and its changelog line omits the breaking changes.

    Under 0.x, minor is the breaking bump, so the level is defensible. But the changelog text only says "regenerate … main additions". The things that will actually break callers (EthSendTransactionBody.to/value/data removed, SolSendTransactionBody.signWith → signWiths, Oauth2AuthenticateBody.nonce required, v1FiatOnRampBlockchainNetwork.BITCOIN removed, TVC intent fields) are only in the PR description, which nobody reads from PyPI. Copy the "Breaking changes" section into the changeset.


Verdict: Merge after fixes — finding 1 blocks (or land #21 in the same release and verify it covers ResultV\d+ keys in send_signed_request). Finding 2 should ship together with #21 or a larger default polling budget. 3 and 4 are non-blocking.

Attacked but held:

  • Schema tampering. schema/public_api.swagger.json is byte-identical to tkhq/sdk@7b456b0:packages/sdk-types/src/__inputs__/public_api.swagger.json.
  • Hand edits / non-determinism in generated files. Regenerated from a clean venv with current ruff: zero diff against the committed client.py and types.py.
  • Query/activity misclassification. For all 189 operations, the name-prefix classifier agrees with the schema shape (request has type enum + parameters ⇔ activity). validateTvcImage is the only query not covered by the old prefixes. Every request type enum has exactly one value; every one of those values exists in v1ActivityType.
  • Stale pins. All 16 pinned types are still in v1ActivityType, and all pinned intent/result definitions still exist. V7 sub-org result unpacks correctly via mock.
  • Result-key mismatches on the typed path. createSwapQuote V3 → createSwapQuoteResultV2 and executeSwap V3 → executeSwapResult match what the backend runners write. Completed eth_send_transaction V2 unpacks sendTransactionStatusId on the typed path.
  • Name-derived vs schema-derived types. Only ethUndelegate7702 differs (_UNDELEGATE7702 vs _UNDELEGATE_7702); the new logic sends the schema one.
  • extract_latest_versions string compare ("V10" < "V9"). No definition has a two-digit version suffix yet. Worth a numeric compare eventually.
  • Pydantic field-name hazards. No new property names collide with Python keywords, model_ prefix, or BaseModel attributes; from is aliased on all four types that have it (including the new v1EthTransactionHistoryItem). python -W error -c "import turnkey_sdk_types" is clean.
  • v1User.mfaPolicies required. I confirmed a response without it fails get_users with ValidationError, but the public API gateway uses grpc-gateway v2's default marshaler (EmitUnpopulated: true), so empty repeated fields are emitted as []. Same situation as the already-required userTags / oauthProviders.
  • Duplicate snake_case method names after conversion. None.
  • make typecheck, make format-check (with the new *.md exclude), and the sdk-types test all pass locally.

Review provenance

Human asked about:

  • An adversarial code review and security audit of this PR, posted as a single comment.

Answered by: automated review, run and checked locally by the commenter
Reviewed commit: e4455e0826eb1ca8e6b911869c632198c807bd23

figitaki and others added 3 commits October 17, 2026 16:30
send_signed_request found the result to flatten by scanning dir(result) for names
ending in Result, so a completed eth_send_transaction, sol_send_transaction, or
create_oauth_providers (keys ending in ResultV2) came back with nothing flattened and a
typed response failed validation. It now flattens whichever result field is set.

The default polling budget was 3 retries at 1 second. Send, swap, earn, and TVC
activities often take longer, so the default is now 10 retries at 1 second.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ion drift

CREATE_OAUTH_PROVIDERS, ETH_SEND_TRANSACTION, and SOL_SEND_TRANSACTION are pinned to
V2, the versions this regen moved them to. codegen/activity_versions.json records the
activity type every method sends; make generate-http stops when a method would move
and says how to pin it or accept the move.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@figitaki

Copy link
Copy Markdown
Contributor Author

(see above)

@figitaki
figitaki merged commit 0a1b8ab into main Oct 18, 2026
5 checks passed
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.

3 participants