Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/codegen-bump-2026-10.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
type: minor
packages: [ http, sdk-types ]
changelog: |-
Regenerate the SDK from the current public API schema. Main additions: send transaction V2 (eth_send_transaction and sol_send_transaction now use the V2 intents), swaps, earn, secrets, and transaction history. The default polling budget is now 10 retries at 1 second (was 3), and send_signed_request flattens versioned result keys such as ethSendTransactionResultV2.

Breaking changes:
- EthSendTransactionBody uses the V2 intent: `to`, `value`, and `data` are gone; `calls: List[v1EthCallParams]` is required.
- SolSendTransactionBody uses the V2 intent: `signWith` is gone; `signWiths: List[str]` is required.
- CreateOauthProvidersBody.oauthProviders is `List[v1OauthProviderParamsV2]`, and the method sends V2.
- Oauth2AuthenticateBody.nonce is required.
- v1User.mfaPolicies is a required response field.
- v1FiatOnRampBlockchainNetwork no longer has FIAT_ON_RAMP_BLOCKCHAIN_NETWORK_BITCOIN.
- v1CreateTvcDeploymentIntent drops pivotBindAddresses and requires healthCheckType, healthCheckPort, and publicIngressPort.
6 changes: 6 additions & 0 deletions codegen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,11 @@ make generate-http # Generate HTTP client only
- `pydantic_helpers.py` - Helper functions for Pydantic model generation
- `generate_types.py` - Generates type definitions
- `generate_http.py` - Generates HTTP client
- `constants.py` - Pinned activity versions (`VERSIONED_ACTIVITY_TYPES`) and other fixed tables
- `activity_versions.json` - Snapshot of the activity type each client method sends

## Activity versions

Unpinned activities take their version from the schema's request definition, so a new schema can move a method to a new version. `make generate-http` compares the versions it is about to emit with `activity_versions.json` and stops if any method moved. To keep the old version, pin it in `constants.py`. To accept the move, list it in the PR and rerun with `CODEGEN_ACCEPT_VERSION_CHANGES=1 make generate-http`, which rewrites the snapshot.

This is not a package and is not published to PyPI. It's only used during development.
116 changes: 116 additions & 0 deletions codegen/activity_versions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
{
"claim_earn_fees": "ACTIVITY_TYPE_CLAIM_EARN_FEES",
"claim_swap_fees": "ACTIVITY_TYPE_CLAIM_SWAP_FEES",
"create_api_keys": "ACTIVITY_TYPE_CREATE_API_KEYS_V2",
"create_authenticators": "ACTIVITY_TYPE_CREATE_AUTHENTICATORS_V2",
"create_fiat_on_ramp_credential": "ACTIVITY_TYPE_CREATE_FIAT_ON_RAMP_CREDENTIAL",
"create_invitations": "ACTIVITY_TYPE_CREATE_INVITATIONS",
"create_mfa_policy": "ACTIVITY_TYPE_CREATE_MFA_POLICY",
"create_oauth2_credential": "ACTIVITY_TYPE_CREATE_OAUTH2_CREDENTIAL",
"create_oauth_providers": "ACTIVITY_TYPE_CREATE_OAUTH_PROVIDERS_V2",
"create_policies": "ACTIVITY_TYPE_CREATE_POLICIES",
"create_policy": "ACTIVITY_TYPE_CREATE_POLICY_V3",
"create_private_key_tag": "ACTIVITY_TYPE_CREATE_PRIVATE_KEY_TAG",
"create_private_keys": "ACTIVITY_TYPE_CREATE_PRIVATE_KEYS_V2",
"create_read_only_session": "ACTIVITY_TYPE_CREATE_READ_ONLY_SESSION",
"create_read_write_session": "ACTIVITY_TYPE_CREATE_READ_WRITE_SESSION_V2",
"create_session_profile": "ACTIVITY_TYPE_CREATE_SESSION_PROFILE",
"create_smart_contract_interface": "ACTIVITY_TYPE_CREATE_SMART_CONTRACT_INTERFACE",
"create_sub_organization": "ACTIVITY_TYPE_CREATE_SUB_ORGANIZATION_V7",
"create_swap_quote": "ACTIVITY_TYPE_CREATE_SWAP_QUOTE_V3",
"create_tvc_app": "ACTIVITY_TYPE_CREATE_TVC_APP",
"create_tvc_deployment": "ACTIVITY_TYPE_CREATE_TVC_DEPLOYMENT",
"create_tvc_manifest_approvals": "ACTIVITY_TYPE_CREATE_TVC_MANIFEST_APPROVALS",
"create_tvc_operator": "ACTIVITY_TYPE_CREATE_TVC_OPERATOR",
"create_tvc_quorum_key": "ACTIVITY_TYPE_CREATE_TVC_QUORUM_KEY",
"create_user_tag": "ACTIVITY_TYPE_CREATE_USER_TAG",
"create_users": "ACTIVITY_TYPE_CREATE_USERS_V3",
"create_wallet": "ACTIVITY_TYPE_CREATE_WALLET",
"create_wallet_accounts": "ACTIVITY_TYPE_CREATE_WALLET_ACCOUNTS",
"create_webhook_endpoint": "ACTIVITY_TYPE_CREATE_WEBHOOK_ENDPOINT",
"delete_api_keys": "ACTIVITY_TYPE_DELETE_API_KEYS",
"delete_authenticators": "ACTIVITY_TYPE_DELETE_AUTHENTICATORS",
"delete_fiat_on_ramp_credential": "ACTIVITY_TYPE_DELETE_FIAT_ON_RAMP_CREDENTIAL",
"delete_invitation": "ACTIVITY_TYPE_DELETE_INVITATION",
"delete_mfa_policy": "ACTIVITY_TYPE_DELETE_MFA_POLICY",
"delete_oauth2_credential": "ACTIVITY_TYPE_DELETE_OAUTH2_CREDENTIAL",
"delete_oauth_providers": "ACTIVITY_TYPE_DELETE_OAUTH_PROVIDERS",
"delete_policies": "ACTIVITY_TYPE_DELETE_POLICIES",
"delete_policy": "ACTIVITY_TYPE_DELETE_POLICY",
"delete_private_key_tags": "ACTIVITY_TYPE_DELETE_PRIVATE_KEY_TAGS",
"delete_private_keys": "ACTIVITY_TYPE_DELETE_PRIVATE_KEYS",
"delete_secrets": "ACTIVITY_TYPE_DELETE_SECRETS",
"delete_smart_contract_interface": "ACTIVITY_TYPE_DELETE_SMART_CONTRACT_INTERFACE",
"delete_sub_organization": "ACTIVITY_TYPE_DELETE_SUB_ORGANIZATION",
"delete_tvc_app_and_deployments": "ACTIVITY_TYPE_DELETE_TVC_APP_AND_DEPLOYMENTS",
"delete_tvc_deployment": "ACTIVITY_TYPE_DELETE_TVC_DEPLOYMENT",
"delete_user_tags": "ACTIVITY_TYPE_DELETE_USER_TAGS",
"delete_users": "ACTIVITY_TYPE_DELETE_USERS",
"delete_wallet_accounts": "ACTIVITY_TYPE_DELETE_WALLET_ACCOUNTS",
"delete_wallets": "ACTIVITY_TYPE_DELETE_WALLETS",
"delete_webhook_endpoint": "ACTIVITY_TYPE_DELETE_WEBHOOK_ENDPOINT",
"earn_claim_rewards": "ACTIVITY_TYPE_EARN_CLAIM_REWARDS",
"earn_deploy_wrapper": "ACTIVITY_TYPE_EARN_DEPLOY_WRAPPER",
"earn_deposit": "ACTIVITY_TYPE_EARN_DEPOSIT",
"earn_set_wrapper_state": "ACTIVITY_TYPE_EARN_SET_WRAPPER_STATE",
"earn_withdraw": "ACTIVITY_TYPE_EARN_WITHDRAW",
"email_auth": "ACTIVITY_TYPE_EMAIL_AUTH_V3",
"eth_send_transaction": "ACTIVITY_TYPE_ETH_SEND_TRANSACTION_V2",
"eth_undelegate7702": "ACTIVITY_TYPE_ETH_UNDELEGATE_7702",
"execute_swap": "ACTIVITY_TYPE_EXECUTE_SWAP_V3",
"export_private_key": "ACTIVITY_TYPE_EXPORT_PRIVATE_KEY",
"export_secrets": "ACTIVITY_TYPE_EXPORT_SECRETS",
"export_wallet": "ACTIVITY_TYPE_EXPORT_WALLET",
"export_wallet_account": "ACTIVITY_TYPE_EXPORT_WALLET_ACCOUNT",
"import_private_key": "ACTIVITY_TYPE_IMPORT_PRIVATE_KEY",
"import_secrets": "ACTIVITY_TYPE_IMPORT_SECRETS",
"import_wallet": "ACTIVITY_TYPE_IMPORT_WALLET",
"init_fiat_on_ramp": "ACTIVITY_TYPE_INIT_FIAT_ON_RAMP",
"init_import_private_key": "ACTIVITY_TYPE_INIT_IMPORT_PRIVATE_KEY",
"init_import_secrets": "ACTIVITY_TYPE_INIT_IMPORT_SECRETS",
"init_import_wallet": "ACTIVITY_TYPE_INIT_IMPORT_WALLET",
"init_otp": "ACTIVITY_TYPE_INIT_OTP_V3",
"init_otp_auth": "ACTIVITY_TYPE_INIT_OTP_AUTH_V3",
"init_user_email_recovery": "ACTIVITY_TYPE_INIT_USER_EMAIL_RECOVERY_V2",
"oauth": "ACTIVITY_TYPE_OAUTH",
"oauth2_authenticate": "ACTIVITY_TYPE_OAUTH2_AUTHENTICATE",
"oauth_login": "ACTIVITY_TYPE_OAUTH_LOGIN",
"otp_auth": "ACTIVITY_TYPE_OTP_AUTH",
"otp_login": "ACTIVITY_TYPE_OTP_LOGIN_V2",
"post_tvc_quorum_key_share": "ACTIVITY_TYPE_POST_TVC_QUORUM_KEY_SHARE",
"re_encrypt_tvc_quorum_key_share": "ACTIVITY_TYPE_RE_ENCRYPT_TVC_QUORUM_KEY_SHARE",
"recover_user": "ACTIVITY_TYPE_RECOVER_USER",
"remove_ip_allowlist": "ACTIVITY_TYPE_REMOVE_IP_ALLOWLIST",
"remove_organization_feature": "ACTIVITY_TYPE_REMOVE_ORGANIZATION_FEATURE",
"restore_tvc_deployment": "ACTIVITY_TYPE_RESTORE_TVC_DEPLOYMENT",
"set_ip_allowlist": "ACTIVITY_TYPE_SET_IP_ALLOWLIST",
"set_organization_feature": "ACTIVITY_TYPE_SET_ORGANIZATION_FEATURE",
"sign_raw_payload": "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
"sign_raw_payloads": "ACTIVITY_TYPE_SIGN_RAW_PAYLOADS",
"sign_transaction": "ACTIVITY_TYPE_SIGN_TRANSACTION_V2",
"sol_send_transaction": "ACTIVITY_TYPE_SOL_SEND_TRANSACTION_V2",
"spark_claim_transfer": "ACTIVITY_TYPE_SPARK_CLAIM_TRANSFER",
"spark_prepare_lightning_receive": "ACTIVITY_TYPE_SPARK_PREPARE_LIGHTNING_RECEIVE",
"spark_prepare_transfer": "ACTIVITY_TYPE_SPARK_PREPARE_TRANSFER",
"spark_sign_frost": "ACTIVITY_TYPE_SPARK_SIGN_FROST",
"stamp_login": "ACTIVITY_TYPE_STAMP_LOGIN",
"update_fiat_on_ramp_credential": "ACTIVITY_TYPE_UPDATE_FIAT_ON_RAMP_CREDENTIAL",
"update_mfa_policy": "ACTIVITY_TYPE_UPDATE_MFA_POLICY",
"update_oauth2_credential": "ACTIVITY_TYPE_UPDATE_OAUTH2_CREDENTIAL",
"update_organization_name": "ACTIVITY_TYPE_UPDATE_ORGANIZATION_NAME",
"update_policy": "ACTIVITY_TYPE_UPDATE_POLICY_V2",
"update_private_key_tag": "ACTIVITY_TYPE_UPDATE_PRIVATE_KEY_TAG",
"update_root_quorum": "ACTIVITY_TYPE_UPDATE_ROOT_QUORUM",
"update_tvc_app_live_deployment": "ACTIVITY_TYPE_UPDATE_TVC_APP_LIVE_DEPLOYMENT",
"update_user": "ACTIVITY_TYPE_UPDATE_USER",
"update_user_email": "ACTIVITY_TYPE_UPDATE_USER_EMAIL",
"update_user_name": "ACTIVITY_TYPE_UPDATE_USER_NAME",
"update_user_phone_number": "ACTIVITY_TYPE_UPDATE_USER_PHONE_NUMBER",
"update_user_tag": "ACTIVITY_TYPE_UPDATE_USER_TAG",
"update_wallet": "ACTIVITY_TYPE_UPDATE_WALLET",
"update_webhook_endpoint": "ACTIVITY_TYPE_UPDATE_WEBHOOK_ENDPOINT",
"upsert_swap_config": "ACTIVITY_TYPE_UPSERT_SWAP_CONFIG",
"upsert_swap_fee_sponsorship_limit_config": "ACTIVITY_TYPE_UPSERT_SWAP_FEE_SPONSORSHIP_LIMIT_CONFIG",
"upsert_swap_fixed_rate_limit_config": "ACTIVITY_TYPE_UPSERT_SWAP_FIXED_RATE_LIMIT_CONFIG",
"verify_otp": "ACTIVITY_TYPE_VERIFY_OTP_V2"
}
17 changes: 17 additions & 0 deletions codegen/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,23 @@
"v1OtpLoginIntentV2",
"v1OtpLoginResult",
),
# Moved to V2 in the October 2026 regen, in step with the TypeScript SDK. Pinned so a
# later schema cannot move them again without a visible diff here.
"ACTIVITY_TYPE_CREATE_OAUTH_PROVIDERS": (
"ACTIVITY_TYPE_CREATE_OAUTH_PROVIDERS_V2",
"v1CreateOauthProvidersIntentV2",
"v1CreateOauthProvidersResultV2",
),
"ACTIVITY_TYPE_ETH_SEND_TRANSACTION": (
"ACTIVITY_TYPE_ETH_SEND_TRANSACTION_V2",
"v1EthSendTransactionIntentV2",
"v1EthSendTransactionResultV2",
),
"ACTIVITY_TYPE_SOL_SEND_TRANSACTION": (
"ACTIVITY_TYPE_SOL_SEND_TRANSACTION_V2",
"v1SolSendTransactionIntentV2",
"v1SolSendTransactionResultV2",
),
}

# Methods that have only optional parameters
Expand Down
118 changes: 103 additions & 15 deletions codegen/http/generate_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"""Generate HTTP client from OpenAPI specification."""

import json
import re
import subprocess
import sys
from pathlib import Path
Expand All @@ -21,6 +22,7 @@
extract_latest_versions,
method_type_from_method_name,
resolve_versioned_activity_type,
get_versioned_result_type,
)

# Get the project root directory
Expand All @@ -30,6 +32,65 @@
OUTPUT_FILE = OUTPUT_DIR / "client.py"


def get_request_activity_type(
definitions: Dict[str, Any], operation: Dict[str, Any]
) -> str | None:
"""Return the activity type from the request definition's "type" enum, if any."""
for param in operation.get("parameters", []):
if param.get("in") == "body" and "$ref" in param.get("schema", {}):
request_type_name = param["schema"]["$ref"].replace("#/definitions/", "")
request_def = definitions.get(request_type_name, {})
enum = request_def.get("properties", {}).get("type", {}).get("enum")
if enum:
return enum[0]
return None


VERSIONS_FILE = PROJECT_ROOT / "codegen" / "activity_versions.json"

# Filled while generating: snake_case method name -> activity type the client sends.
SENT_ACTIVITY_TYPES: Dict[str, str] = {}


def check_activity_versions(sent: Dict[str, str]) -> None:
"""Fail when a method's sent activity type differs from codegen/activity_versions.json.

A regen takes unpinned activity versions from the schema, so a new schema can move a
method to a new version with only a one-line change buried in client.py. This makes
every such move a visible diff in the snapshot. Set CODEGEN_ACCEPT_VERSION_CHANGES=1
to rewrite the snapshot after reviewing the moves.
"""
import os

previous: Dict[str, str] = {}
if VERSIONS_FILE.exists():
previous = json.loads(VERSIONS_FILE.read_text())
moved = {
name: (previous.get(name), sent[name])
for name in sorted(sent)
if previous.get(name) != sent[name]
}
if not moved and VERSIONS_FILE.exists():
return
if (
moved
and VERSIONS_FILE.exists()
and not os.environ.get("CODEGEN_ACCEPT_VERSION_CHANGES")
):
lines = [
f" {name}: {old or '(new)'} -> {new}" for name, (old, new) in moved.items()
]
raise SystemExit(
"Activity versions changed for these methods:\n"
+ "\n".join(lines)
+ "\nPin them in codegen/constants.py to keep the old version, or list the moves "
"in the PR and rerun with CODEGEN_ACCEPT_VERSION_CHANGES=1 to update "
f"{VERSIONS_FILE.name}."
)
VERSIONS_FILE.write_text(json.dumps(dict(sorted(sent.items())), indent=2) + "\n")
print(f"📝 Updated {VERSIONS_FILE} ({len(moved)} change(s))")


def generate_sdk_client(swagger: Dict[str, Any]) -> str:
"""Generate the SDK client from Swagger spec."""
namespace = next(
Expand All @@ -50,7 +111,7 @@ def __init__(
organization_id: str,
default_timeout: int = 30,
polling_interval_ms: int = 1000,
max_polling_retries: int = 3
max_polling_retries: int = 10
):
\"\"\"Initialize the Turnkey client.

Expand Down Expand Up @@ -292,18 +353,15 @@ def send_signed_request(self, signed_request: SignedRequest, response_type: type
activity_dict = activity.model_dump(by_alias=True, exclude_none=True) if hasattr(activity, 'model_dump') else {}
payload["activity"] = activity_dict

# Extract result fields if activity completed successfully
if activity.status == "ACTIVITY_STATUS_COMPLETED" and hasattr(activity, 'result') and activity.result:
result = activity.result
# Find the first result field (e.g., createApiKeysResult, createPolicyResult, etc.)
for attr_name in dir(result):
if not attr_name.startswith('_') and attr_name.endswith('Result'):
result_data = getattr(result, attr_name, None)
if result_data and hasattr(result_data, 'model_dump'):
# Flatten result fields into payload
result_dict = result_data.model_dump(by_alias=True, exclude_none=True)
payload.update(result_dict)
break
# Flatten the result that is set, whatever its version suffix
# (e.g., createPolicyResult, ethSendTransactionResultV2, createSubOrganizationResultV7)
result = activity.result
if activity.status == "ACTIVITY_STATUS_COMPLETED" and result is not None:
for field_name in type(result).model_fields:
result_data = getattr(result, field_name, None)
if result_data is not None:
payload.update(result_data.model_dump(by_alias=True, exclude_none=True))
break

return response_type(**payload) if response_type is not None else payload
""")
Expand Down Expand Up @@ -425,8 +483,36 @@ def stamp_{snake_method_name}(self, input: {input_type}) -> SignedRequest:
""")

elif method_type == "activity":
result_key = operation_name_without_namespace + "Result"
versioned_method_name = latest_versions[result_key]["formatted_key_name"]
# Use the activity type from the request definition. A pinned
# version in VERSIONED_ACTIVITY_TYPES takes precedence.
schema_activity_type = get_request_activity_type(
swagger["definitions"], operation
)
if schema_activity_type:
unversioned_activity_type = re.sub(r"_V\d+$", "", schema_activity_type)
pinned_activity_type = resolve_versioned_activity_type(
unversioned_activity_type
)
versioned_activity_type = (
pinned_activity_type
if pinned_activity_type != unversioned_activity_type
else schema_activity_type
)

SENT_ACTIVITY_TYPES[snake_method_name] = versioned_activity_type

# Use the pinned result type if there is one, else the latest version
pinned_result_type = get_versioned_result_type(unversioned_activity_type)
if pinned_result_type and pinned_result_type in swagger["definitions"]:
pinned_result_key = re.sub(r"^v\d+", "", pinned_result_type)
versioned_method_name = (
pinned_result_key[0].lower() + pinned_result_key[1:]
)
else:
result_key = operation_name_without_namespace + "Result"
versioned_method_name = latest_versions[result_key][
"formatted_key_name"
]

code_buffer.append(f"""
def {snake_method_name}(self, input: {input_type}) -> {response_type}:
Expand Down Expand Up @@ -555,6 +641,8 @@ def main():
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

# Write output
check_activity_versions(SENT_ACTIVITY_TYPES)

with open(OUTPUT_FILE, "w") as f:
f.write(output)

Expand Down
2 changes: 2 additions & 0 deletions codegen/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,11 @@ def method_type_from_method_name(method_name: str) -> str:
method_name_lower.startswith("get")
or method_name_lower.startswith("list")
or method_name_lower.startswith("test")
or method_name_lower.startswith("validate")
or method_name_lower.startswith("tget")
or method_name_lower.startswith("tlist")
or method_name_lower.startswith("ttest")
or method_name_lower.startswith("tvalidate")
):
return "query"
return "activity"
Expand Down
Loading
Loading