chipi-stack 2.2.0__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. {chipi_stack-2.2.0/chipi_stack.egg-info → chipi_stack-2.3.0}/PKG-INFO +1 -1
  2. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/__init__.py +1 -1
  3. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/models/wallet.py +34 -21
  4. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/shhh/__init__.py +93 -77
  5. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/shhh/signers/__init__.py +28 -4
  6. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/shhh/signers/ed25519.py +81 -17
  7. chipi_stack-2.3.0/chipi_sdk/shhh/signers/webauthn.py +241 -0
  8. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/wallets.py +138 -77
  9. {chipi_stack-2.2.0 → chipi_stack-2.3.0/chipi_stack.egg-info}/PKG-INFO +1 -1
  10. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_stack.egg-info/SOURCES.txt +1 -0
  11. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/pyproject.toml +1 -1
  12. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/LICENSE +0 -0
  13. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/MANIFEST.in +0 -0
  14. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/README.md +0 -0
  15. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/client.py +0 -0
  16. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/constants.py +0 -0
  17. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/encryption.py +0 -0
  18. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/errors.py +0 -0
  19. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/execute_paymaster.py +0 -0
  20. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/formatters.py +0 -0
  21. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/models/__init__.py +0 -0
  22. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/models/core.py +0 -0
  23. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/models/session.py +0 -0
  24. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/models/sku.py +0 -0
  25. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/models/sku_purchase.py +0 -0
  26. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/models/sku_transaction.py +0 -0
  27. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/models/transaction.py +0 -0
  28. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/models/user.py +0 -0
  29. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/models/x402.py +0 -0
  30. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/py.typed +0 -0
  31. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/sdk.py +0 -0
  32. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/sessions.py +0 -0
  33. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/shhh/compute_wallet_address.py +0 -0
  34. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/shhh/execute_paymaster_raw.py +0 -0
  35. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/shhh/migrate.py +0 -0
  36. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/shhh/oe.py +0 -0
  37. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/shhh/recovery.py +0 -0
  38. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/shhh/signers/eip191.py +0 -0
  39. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/shhh/signers/jwt_apple_sub.py +0 -0
  40. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/shhh/signers/stark.py +0 -0
  41. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/shhh/snip12_hash.py +0 -0
  42. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/shhh/threshold.py +0 -0
  43. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/sku_purchases.py +0 -0
  44. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/sku_transactions.py +0 -0
  45. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/skus.py +0 -0
  46. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/transactions.py +0 -0
  47. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/users.py +0 -0
  48. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/validators.py +0 -0
  49. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/x402_client.py +0 -0
  50. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/x402_facilitator.py +0 -0
  51. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_sdk/x402_middleware.py +0 -0
  52. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_stack.egg-info/dependency_links.txt +0 -0
  53. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_stack.egg-info/requires.txt +0 -0
  54. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/chipi_stack.egg-info/top_level.txt +0 -0
  55. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/setup.cfg +0 -0
  56. {chipi_stack-2.2.0 → chipi_stack-2.3.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chipi-stack
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: Python SDK for Starknet Gasless Transactions via Chipi
5
5
  Author-email: Carlos Castillo <carlos@chipipay.com>, Roberto Yamanaka <roberto@chipipay.com>
6
6
  License-Expression: MIT
@@ -5,7 +5,7 @@ A Python SDK for interacting with Chipi's gasless transaction infrastructure on
5
5
  Supports wallet creation, transaction execution, session keys, and more.
6
6
  """
7
7
 
8
- __version__ = "2.1.0"
8
+ __version__ = "2.3.0"
9
9
 
10
10
  # Main SDK class
11
11
  from .sdk import ChipiSDK
@@ -3,15 +3,16 @@
3
3
  from datetime import datetime
4
4
  from enum import Enum
5
5
  from typing import Optional
6
- from pydantic import BaseModel, Field, ConfigDict
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
7
8
 
8
9
  from .core import Chain, ChainToken
9
10
 
10
11
 
11
12
  def to_camel(string: str) -> str:
12
13
  """Convert snake_case to camelCase."""
13
- components = string.split('_')
14
- return components[0] + ''.join(x.title() for x in components[1:])
14
+ components = string.split("_")
15
+ return components[0] + "".join(x.title() for x in components[1:])
15
16
 
16
17
 
17
18
  class WalletType(str, Enum):
@@ -26,6 +27,7 @@ class WalletType(str, Enum):
26
27
  JWT_ES256_APPLE_SUB, BLS12_381). Requires CHIPI_SHHH_ENABLED on
27
28
  the backend.
28
29
  """
30
+
29
31
  CHIPI = "CHIPI"
30
32
  READY = "READY"
31
33
  SHHH = "SHHH"
@@ -42,6 +44,7 @@ class ShhhSignerKind(str, Enum):
42
44
  browser/RN context. Wallet creation with wallet_type=SHHH and no
43
45
  signer_kind raises in Python (per plan D10).
44
46
  """
47
+
45
48
  STARK = "STARK"
46
49
  ED25519 = "ED25519"
47
50
  SECP256K1 = "SECP256K1"
@@ -56,9 +59,9 @@ class ShhhSignerKind(str, Enum):
56
59
 
57
60
  class WalletData(BaseModel):
58
61
  """Core wallet data structure."""
59
-
62
+
60
63
  model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
61
-
64
+
62
65
  public_key: str = Field(..., description="Wallet public address")
63
66
  encrypted_private_key: str = Field(..., description="AES encrypted private key")
64
67
  wallet_type: Optional[WalletType] = Field(None, description="Type of wallet")
@@ -67,7 +70,7 @@ class WalletData(BaseModel):
67
70
 
68
71
  class DeploymentData(BaseModel):
69
72
  """Contract deployment data."""
70
-
73
+
71
74
  class_hash: str
72
75
  salt: str
73
76
  unique: str
@@ -96,14 +99,22 @@ class CreateWalletParams(BaseModel):
96
99
  "Required when wallet_type=SHHH is set EXPLICITLY. When SHHH "
97
100
  "is reached via the default-flip (caller omitted wallet_type), "
98
101
  "the SDK falls back to STARK to match the TS SDK and the live "
99
- "on-chain smokes. Wallet CREATION from Python currently "
100
- "supports STARK only non-STARK kinds (ED25519, "
101
- "EIP191_SECP256K1, etc.) raise NotImplementedError at create "
102
- "time and will be enabled by a follow-up PR that adds the "
103
- "external-rooted (MetaMask, Phantom, Apple) wallet-adapter "
104
- "deploy paths. The chipi_sdk.shhh envelope BUILDERS for those "
105
- "kinds remain usable today for signing OEs on existing SHHH "
106
- "wallets (see chipi_sdk.shhh.signers)."
102
+ "on-chain smokes. Supported deploy kinds: STARK + ED25519 "
103
+ "(Chipi-held key), WEBAUTHN_P256 + EIP191_SECP256K1 "
104
+ "(external-rooted supply owner_pubkey_felts). "
105
+ "JWT_ES256_APPLE_SUB and the remaining kinds raise "
106
+ "NotImplementedError at create time."
107
+ ),
108
+ )
109
+ owner_pubkey_felts: Optional[list[str]] = Field(
110
+ None,
111
+ description=(
112
+ "External-rooted SHHH only. The owner's public key as Cairo "
113
+ "felts (hex strings) when Chipi does NOT hold the signing key: "
114
+ "WEBAUTHN_P256 / EIP191_SECP256K1 → [x_low, x_high, y_low, "
115
+ "y_high] (4 felts); external ED25519 (Phantom) → [low, high] "
116
+ "(2 felts). Omit for STARK and Chipi-held ED25519 (the SDK "
117
+ "generates the key)."
107
118
  ),
108
119
  )
109
120
 
@@ -140,7 +151,7 @@ CreateWalletResponse = GetWalletResponse
140
151
 
141
152
  class GetWalletParams(BaseModel):
142
153
  """Parameters for retrieving a wallet."""
143
-
154
+
144
155
  external_user_id: str = Field(..., description="External user identifier")
145
156
 
146
157
 
@@ -157,9 +168,9 @@ class GetTokenBalanceParams(BaseModel):
157
168
 
158
169
  class GetTokenBalanceResponse(BaseModel):
159
170
  """Response from token balance query."""
160
-
171
+
161
172
  model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
162
-
173
+
163
174
  chain: Chain
164
175
  chain_token: ChainToken
165
176
  chain_token_address: str
@@ -169,16 +180,16 @@ class GetTokenBalanceResponse(BaseModel):
169
180
 
170
181
  class PrepareWalletCreationResponse(BaseModel):
171
182
  """Response from wallet creation preparation."""
172
-
183
+
173
184
  model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
174
-
185
+
175
186
  typed_data: dict
176
187
  account_class_hash: str
177
188
 
178
189
 
179
190
  class CreateCustodialWalletParams(BaseModel):
180
191
  """Parameters for creating a custodial wallet."""
181
-
192
+
182
193
  chain: Chain
183
194
  org_id: str
184
195
 
@@ -198,7 +209,9 @@ class PrepareWalletUpgradeParams(BaseModel):
198
209
  """Parameters for preparing a wallet upgrade."""
199
210
 
200
211
  wallet_address: str = Field(..., description="Wallet address to upgrade")
201
- target_class_hash: Optional[str] = Field(None, description="Target class hash (defaults to latest CHIPI)")
212
+ target_class_hash: Optional[str] = Field(
213
+ None, description="Target class hash (defaults to latest CHIPI)"
214
+ )
202
215
 
203
216
 
204
217
  class PrepareWalletUpgradeResponse(BaseModel):
@@ -1,50 +1,37 @@
1
1
  """SHHH V8.4 ShhhAccount integration surface for `chipi-stack` (Python).
2
2
 
3
- Cycle 1 server-side kinds: STARK, ED25519, EIP191_SECP256K1.
4
- WebAuthn (P-256) is excluded by design — Python has no `navigator.credentials`
5
- context, and the V8.4 verifier dispatches WebAuthn assertions against
6
- browser-collected `authenticatorData` + `clientDataJSON`, both of which
7
- only make sense from a passkey flow. Use `@chipi-stack/chipi-react` (or
8
- `chipi-expo`) for the WebAuthn signer.
3
+ Server-side kinds: STARK, ED25519, EIP191_SECP256K1, WEBAUTHN_P256.
4
+ WebAuthn support is **from-assertion only**: Python can't run
5
+ `navigator.credentials` to *create* a passkey assertion (use
6
+ `@chipi-stack/chipi-react` / `chipi-expo` for that), but it CAN build the
7
+ V2_SNIP12 envelope from an assertion (`authenticatorData` +
8
+ `clientDataJSON` + DER signature) that a browser produced and POSTed to
9
+ your server — the same pattern as `build_eip191_envelope_from_signature`
10
+ for MetaMask. The from-private-key WebAuthn helper is browser/test-only
11
+ and is not ported.
9
12
 
10
13
  This module mirrors `@chipi-stack/backend/src/shhh` byte-for-byte where
11
14
  possible. Parity is validated against `tests/fixtures/cycle1.json` (the
12
15
  same JSON we PR'd to shhh-wallet-cairo as drift-detection fixtures).
13
16
  """
14
17
 
15
- from .snip12_hash import (
16
- CALL_TYPE_HASH_REV1,
17
- ISIGNER_CANONICAL_LABEL,
18
- ISIGNER_ID,
19
- ISRC9_V2_ID,
20
- OE_DOMAIN_NAME,
21
- OE_DOMAIN_REVISION,
22
- OE_DOMAIN_VERSION,
23
- OUTSIDE_EXECUTION_TYPE_HASH_REV1,
24
- SIG_VERSION_V1_HEX_ASCII,
25
- SIG_VERSION_V2_SNIP12,
26
- SIG_VERSION_V2_THRESHOLD,
27
- STARKNET_DOMAIN_TYPE_HASH_REV1,
28
- STARKNET_MESSAGE_PREFIX,
29
- Call,
30
- OutsideExecution,
31
- compute_isigner_id,
32
- compute_snip12_hash,
33
- hash_call,
34
- hash_calls,
35
- hash_outside_execution_struct,
36
- hash_starknet_domain,
37
- snip12_hash_to_hex_ascii_bytes,
38
- starknet_keccak,
39
- )
40
-
41
18
  from .compute_wallet_address import (
42
19
  ComputeAddressInput,
43
20
  compute_address_salt,
44
21
  compute_shhh_address,
45
22
  owner_commitment,
46
23
  )
47
-
24
+ from .migrate import (
25
+ BOOTSTRAP_DOMAIN_SEPARATOR,
26
+ BootstrapMessageParams,
27
+ BuildBootstrapSignedCallParams,
28
+ BuildMigrationCallsParams,
29
+ build_bootstrap_signed_call,
30
+ build_migration_calls,
31
+ compute_bootstrap_message,
32
+ label_to_felt,
33
+ sign_bootstrap_message,
34
+ )
48
35
  from .oe import (
49
36
  ANY_CALLER_FELT,
50
37
  MAX_OE_VALIDITY_WINDOW_SECONDS,
@@ -54,23 +41,55 @@ from .oe import (
54
41
  wrap_v2_snip12_envelope,
55
42
  wrap_v2_threshold_envelope,
56
43
  )
57
-
44
+ from .recovery import (
45
+ DEFAULT_OP_EXPIRY_SECONDS,
46
+ OP_ADD_OWNER,
47
+ OP_REMOVE_OWNER,
48
+ OP_ROTATE_OWNER,
49
+ OP_SET_THRESHOLD,
50
+ ROLE_GUARDIAN,
51
+ ROLE_OWNER,
52
+ TIMELOCK_SECONDS,
53
+ VIEW_SELECTORS,
54
+ NewOwnerInput,
55
+ OwnerRole,
56
+ build_cancel_pending_op_call,
57
+ build_cancel_recovery_call,
58
+ build_execute_add_owner_call,
59
+ build_execute_remove_owner_call,
60
+ build_execute_rotate_owner_call,
61
+ build_execute_set_threshold_call,
62
+ build_finalize_recovery_call,
63
+ build_initiate_recovery_call,
64
+ build_propose_add_owner_call,
65
+ build_propose_remove_owner_call,
66
+ build_propose_rotate_owner_call,
67
+ build_propose_set_threshold_call,
68
+ is_ready_to_execute,
69
+ seconds_until_executable,
70
+ )
58
71
  from .signers import (
59
72
  APPLE_ISSUER,
60
- AppleTokenResponse,
61
73
  ED25519_KIND_TAG,
62
74
  EIP191_KIND_TAG,
63
75
  JWT_APPLE_SUB_KIND_TAG,
64
76
  JWT_APPLE_SUB_KIND_TAG_FELT,
65
- JwtAppleSubPubkey,
66
77
  STARK_KIND_TAG,
78
+ WEBAUTHN_KIND_TAG,
79
+ AppleTokenResponse,
67
80
  Eip191Pubkey,
81
+ JwtAppleSubPubkey,
82
+ WebAuthnPubkey,
83
+ base64url_encode,
68
84
  build_ed25519_envelope,
85
+ build_ed25519_envelope_from_signature,
69
86
  build_eip191_envelope_from_private_key,
70
87
  build_eip191_envelope_from_signature,
71
88
  build_jwt_apple_sub_envelope_from_test_key,
72
89
  build_jwt_apple_sub_envelope_from_token_response,
73
90
  build_stark_envelope,
91
+ build_webauthn_envelope_from_assertion,
92
+ byte_array_to_felts,
74
93
  ed25519_owner_id,
75
94
  ed25519_pubkey_felts,
76
95
  ed25519_signed_bytes,
@@ -82,11 +101,38 @@ from .signers import (
82
101
  locate_sub_claim,
83
102
  message_hash_to_nonce_b64url,
84
103
  parse_apple_id_token,
104
+ parse_der_ecdsa_signature,
85
105
  parse_eip191_signature,
86
106
  stark_owner_id,
87
107
  stark_pubkey_felts,
108
+ webauthn_owner_id,
109
+ webauthn_pubkey_felts,
110
+ )
111
+ from .snip12_hash import (
112
+ CALL_TYPE_HASH_REV1,
113
+ ISIGNER_CANONICAL_LABEL,
114
+ ISIGNER_ID,
115
+ ISRC9_V2_ID,
116
+ OE_DOMAIN_NAME,
117
+ OE_DOMAIN_REVISION,
118
+ OE_DOMAIN_VERSION,
119
+ OUTSIDE_EXECUTION_TYPE_HASH_REV1,
120
+ SIG_VERSION_V1_HEX_ASCII,
121
+ SIG_VERSION_V2_SNIP12,
122
+ SIG_VERSION_V2_THRESHOLD,
123
+ STARKNET_DOMAIN_TYPE_HASH_REV1,
124
+ STARKNET_MESSAGE_PREFIX,
125
+ Call,
126
+ OutsideExecution,
127
+ compute_isigner_id,
128
+ compute_snip12_hash,
129
+ hash_call,
130
+ hash_calls,
131
+ hash_outside_execution_struct,
132
+ hash_starknet_domain,
133
+ snip12_hash_to_hex_ascii_bytes,
134
+ starknet_keccak,
88
135
  )
89
-
90
136
  from .threshold import (
91
137
  MIN_THRESHOLD_OWNERS,
92
138
  assert_valid_v2_snip12_envelope,
@@ -95,46 +141,6 @@ from .threshold import (
95
141
  parse_threshold_envelope,
96
142
  )
97
143
 
98
- from .recovery import (
99
- DEFAULT_OP_EXPIRY_SECONDS,
100
- NewOwnerInput,
101
- OP_ADD_OWNER,
102
- OP_REMOVE_OWNER,
103
- OP_ROTATE_OWNER,
104
- OP_SET_THRESHOLD,
105
- OwnerRole,
106
- ROLE_GUARDIAN,
107
- ROLE_OWNER,
108
- TIMELOCK_SECONDS,
109
- VIEW_SELECTORS,
110
- build_cancel_pending_op_call,
111
- build_cancel_recovery_call,
112
- build_execute_add_owner_call,
113
- build_execute_remove_owner_call,
114
- build_execute_rotate_owner_call,
115
- build_execute_set_threshold_call,
116
- build_finalize_recovery_call,
117
- build_initiate_recovery_call,
118
- build_propose_add_owner_call,
119
- build_propose_remove_owner_call,
120
- build_propose_rotate_owner_call,
121
- build_propose_set_threshold_call,
122
- is_ready_to_execute,
123
- seconds_until_executable,
124
- )
125
-
126
- from .migrate import (
127
- BOOTSTRAP_DOMAIN_SEPARATOR,
128
- BootstrapMessageParams,
129
- BuildBootstrapSignedCallParams,
130
- BuildMigrationCallsParams,
131
- build_bootstrap_signed_call,
132
- build_migration_calls,
133
- compute_bootstrap_message,
134
- label_to_felt,
135
- sign_bootstrap_message,
136
- )
137
-
138
144
  __all__ = [
139
145
  # snip12_hash
140
146
  "CALL_TYPE_HASH_REV1",
@@ -187,9 +193,19 @@ __all__ = [
187
193
  "parse_eip191_signature",
188
194
  "ED25519_KIND_TAG",
189
195
  "build_ed25519_envelope",
196
+ "build_ed25519_envelope_from_signature",
190
197
  "ed25519_owner_id",
191
198
  "ed25519_pubkey_felts",
192
199
  "ed25519_signed_bytes",
200
+ # webauthn signer (from-assertion)
201
+ "WEBAUTHN_KIND_TAG",
202
+ "WebAuthnPubkey",
203
+ "base64url_encode",
204
+ "build_webauthn_envelope_from_assertion",
205
+ "byte_array_to_felts",
206
+ "parse_der_ecdsa_signature",
207
+ "webauthn_owner_id",
208
+ "webauthn_pubkey_felts",
193
209
  # jwt_apple_sub signer
194
210
  "APPLE_ISSUER",
195
211
  "AppleTokenResponse",
@@ -1,14 +1,19 @@
1
1
  """SHHH V8.4 signers (Python).
2
2
 
3
- Cycle 1 server-side kinds: STARK, Ed25519, EIP-191 secp256k1.
4
- Wave B kind: JWT_ES256_APPLE_SUB (Apple Sign In).
3
+ Server-side kinds: STARK, Ed25519, EIP-191 secp256k1, WebAuthn P-256
4
+ (from-assertion only), JWT_ES256_APPLE_SUB (Apple Sign In).
5
5
 
6
- WebAuthn is excluded -- Python has no browser passkey context.
6
+ WebAuthn note: Python can't run ``navigator.credentials`` to *create*
7
+ assertions, but it CAN build an envelope from one a browser produced and
8
+ POSTed — the same server-side pattern as
9
+ ``build_eip191_envelope_from_signature``. Only the from-assertion builder
10
+ is ported (no from-private-key helper).
7
11
  """
8
12
 
9
13
  from .ed25519 import (
10
14
  ED25519_KIND_TAG,
11
15
  build_ed25519_envelope,
16
+ build_ed25519_envelope_from_signature,
12
17
  ed25519_owner_id,
13
18
  ed25519_pubkey_felts,
14
19
  ed25519_signed_bytes,
@@ -24,9 +29,9 @@ from .eip191 import (
24
29
  )
25
30
  from .jwt_apple_sub import (
26
31
  APPLE_ISSUER,
27
- AppleTokenResponse,
28
32
  JWT_APPLE_SUB_KIND_TAG,
29
33
  JWT_APPLE_SUB_KIND_TAG_FELT,
34
+ AppleTokenResponse,
30
35
  JwtAppleSubPubkey,
31
36
  build_jwt_apple_sub_envelope_from_test_key,
32
37
  build_jwt_apple_sub_envelope_from_token_response,
@@ -43,6 +48,16 @@ from .stark import (
43
48
  stark_owner_id,
44
49
  stark_pubkey_felts,
45
50
  )
51
+ from .webauthn import (
52
+ WEBAUTHN_KIND_TAG,
53
+ WebAuthnPubkey,
54
+ base64url_encode,
55
+ build_webauthn_envelope_from_assertion,
56
+ byte_array_to_felts,
57
+ parse_der_ecdsa_signature,
58
+ webauthn_owner_id,
59
+ webauthn_pubkey_felts,
60
+ )
46
61
 
47
62
  __all__ = [
48
63
  "STARK_KIND_TAG",
@@ -58,9 +73,18 @@ __all__ = [
58
73
  "parse_eip191_signature",
59
74
  "ED25519_KIND_TAG",
60
75
  "build_ed25519_envelope",
76
+ "build_ed25519_envelope_from_signature",
61
77
  "ed25519_owner_id",
62
78
  "ed25519_pubkey_felts",
63
79
  "ed25519_signed_bytes",
80
+ "WEBAUTHN_KIND_TAG",
81
+ "WebAuthnPubkey",
82
+ "base64url_encode",
83
+ "build_webauthn_envelope_from_assertion",
84
+ "byte_array_to_felts",
85
+ "parse_der_ecdsa_signature",
86
+ "webauthn_owner_id",
87
+ "webauthn_pubkey_felts",
64
88
  "APPLE_ISSUER",
65
89
  "AppleTokenResponse",
66
90
  "JWT_APPLE_SUB_KIND_TAG",
@@ -25,10 +25,8 @@ Implementation choices vs the TS side:
25
25
  and amortized across the process lifetime.
26
26
  """
27
27
 
28
- from typing import Union
29
-
30
- from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
31
28
  from cryptography.hazmat.primitives import serialization
29
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
32
30
  from garaga.starknet.tests_and_calldata_generators.signatures import (
33
31
  EdDSA25519Signature,
34
32
  )
@@ -114,9 +112,7 @@ def build_ed25519_envelope(
114
112
  "owner_commitment or pubkey here."
115
113
  )
116
114
  if len(private_key) != 32:
117
- raise ValueError(
118
- f"Ed25519 private key must be exactly 32 bytes; got {len(private_key)}."
119
- )
115
+ raise ValueError(f"Ed25519 private key must be exactly 32 bytes; got {len(private_key)}.")
120
116
 
121
117
  pk = Ed25519PrivateKey.from_private_bytes(private_key)
122
118
  public_key = pk.public_key().public_bytes(
@@ -128,21 +124,89 @@ def build_ed25519_envelope(
128
124
  signature = pk.sign(signed_bytes)
129
125
 
130
126
  # RFC 8032: signature = R (32B) || s (32B), both little-endian.
131
- R = signature[:32]
132
- s_bytes = signature[32:64]
127
+ return _assemble_ed25519_envelope(
128
+ ry_twisted=int.from_bytes(signature[:32], "little"),
129
+ s_int=int.from_bytes(signature[32:64], "little"),
130
+ py_twisted=int.from_bytes(public_key, "little"),
131
+ signed_bytes=signed_bytes,
132
+ owner_index=owner_index,
133
+ )
134
+
135
+
136
+ def build_ed25519_envelope_from_signature(
137
+ *,
138
+ signature: bytes,
139
+ public_key: bytes,
140
+ message_hash: int,
141
+ owner_index: int = 0,
142
+ ) -> list[int]:
143
+ """Build a V2_SNIP12 Ed25519 envelope from an externally-produced
144
+ signature — the Phantom / Solana flow.
145
+
146
+ Phantom (and every external Ed25519 wallet) never exposes the private
147
+ key: the user signs via ``window.solana.signMessage(...)``, which
148
+ returns a 64-byte signature (R‖s). This is the from-signature
149
+ counterpart of :func:`build_ed25519_envelope`, mirroring
150
+ :func:`build_eip191_envelope_from_signature` for MetaMask and the TS
151
+ ``buildEd25519EnvelopeFromSignature``.
152
+
153
+ The wallet MUST sign the exact bytes returned by
154
+ :func:`ed25519_signed_bytes` (the 64-byte hex-ASCII encoding of the
155
+ SNIP-12 OE hash) — NOT the raw 32-byte ``message_hash``. The Cairo
156
+ verifier reconstructs those same bytes, so a mismatch fails on chain.
157
+
158
+ Inputs:
159
+ signature — 64-byte Ed25519 signature R(32)‖s(32) from the
160
+ wallet's ``signMessage``.
161
+ public_key — 32-byte Ed25519 public key (as Phantom returns it).
162
+ message_hash — SNIP-12 hash of the OE (see oe.compute_oe_hash).
163
+ owner_index — u32 INDEX into the wallet's owner_set. Default 0.
164
+ """
165
+ if not (0 <= owner_index < (1 << 32)):
166
+ raise ValueError(
167
+ f"build_ed25519_envelope_from_signature: owner_index {owner_index} "
168
+ "is out of u32 range [0, 2^32). owner_id is the u32 INDEX into the "
169
+ "wallet's owner_set (0 for primary); do NOT pass an owner_commitment "
170
+ "or pubkey here."
171
+ )
172
+ if len(signature) != 64:
173
+ raise ValueError(f"Ed25519 signature must be exactly 64 bytes (R‖s); got {len(signature)}.")
174
+ if len(public_key) != 32:
175
+ raise ValueError(f"Ed25519 public key must be exactly 32 bytes; got {len(public_key)}.")
176
+
177
+ signed_bytes = ed25519_signed_bytes(message_hash)
178
+ # RFC 8032: signature = R (32B) || s (32B), both little-endian.
179
+ return _assemble_ed25519_envelope(
180
+ ry_twisted=int.from_bytes(signature[:32], "little"),
181
+ s_int=int.from_bytes(signature[32:64], "little"),
182
+ py_twisted=int.from_bytes(public_key, "little"),
183
+ signed_bytes=signed_bytes,
184
+ owner_index=owner_index,
185
+ )
133
186
 
134
- Ry_twisted = int.from_bytes(R, "little")
135
- s_int = int.from_bytes(s_bytes, "little")
136
- Py_twisted = int.from_bytes(public_key, "little")
137
187
 
138
- # Garaga's eddsa_calldata_builder produces:
139
- # [ Ry_low, Ry_high, s_low, s_high,
140
- # msg_len, msg_byte_0, ..., msg_byte_{msg_len-1},
141
- # *msm_hint, *sqrt_Rx_hint, *sqrt_Px_hint ]
188
+ def _assemble_ed25519_envelope(
189
+ *,
190
+ ry_twisted: int,
191
+ s_int: int,
192
+ py_twisted: int,
193
+ signed_bytes: bytes,
194
+ owner_index: int,
195
+ ) -> list[int]:
196
+ """Shared core for both Ed25519 builders: run Garaga's hint builder
197
+ over the signature halves + pubkey + signed message, then wrap in the
198
+ V2_SNIP12 envelope. The locally-signed and from-signature paths differ
199
+ ONLY in where (R, s, pubkey) come from.
200
+
201
+ Garaga's eddsa_calldata_builder produces:
202
+ [ Ry_low, Ry_high, s_low, s_high,
203
+ msg_len, msg_byte_0, ..., msg_byte_{msg_len-1},
204
+ *msm_hint, *sqrt_Rx_hint, *sqrt_Px_hint ]
205
+ """
142
206
  eddsa_sig = EdDSA25519Signature(
143
- Ry_twisted=Ry_twisted,
207
+ Ry_twisted=ry_twisted,
144
208
  s=s_int,
145
- Py_twisted=Py_twisted,
209
+ Py_twisted=py_twisted,
146
210
  msg=signed_bytes,
147
211
  )
148
212
  payload = eddsa_sig.serialize_with_hints(