charm-crypto-framework 0.61.1__cp313-cp313-macosx_10_13_universal2.whl

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 (323) hide show
  1. charm/__init__.py +5 -0
  2. charm/adapters/__init__.py +0 -0
  3. charm/adapters/abenc_adapt_hybrid.py +90 -0
  4. charm/adapters/dabenc_adapt_hybrid.py +145 -0
  5. charm/adapters/ibenc_adapt_hybrid.py +72 -0
  6. charm/adapters/ibenc_adapt_identityhash.py +80 -0
  7. charm/adapters/kpabenc_adapt_hybrid.py +91 -0
  8. charm/adapters/pkenc_adapt_bchk05.py +121 -0
  9. charm/adapters/pkenc_adapt_chk04.py +91 -0
  10. charm/adapters/pkenc_adapt_hybrid.py +98 -0
  11. charm/adapters/pksig_adapt_naor01.py +89 -0
  12. charm/config.py +7 -0
  13. charm/core/__init__.py +0 -0
  14. charm/core/benchmark/benchmark_util.c +353 -0
  15. charm/core/benchmark/benchmark_util.h +61 -0
  16. charm/core/benchmark/benchmarkmodule.c +476 -0
  17. charm/core/benchmark/benchmarkmodule.h +162 -0
  18. charm/core/benchmark.cpython-313-darwin.so +0 -0
  19. charm/core/crypto/AES/AES.c +1464 -0
  20. charm/core/crypto/AES.cpython-313-darwin.so +0 -0
  21. charm/core/crypto/DES/DES.c +113 -0
  22. charm/core/crypto/DES.cpython-313-darwin.so +0 -0
  23. charm/core/crypto/DES3/DES3.c +26 -0
  24. charm/core/crypto/DES3.cpython-313-darwin.so +0 -0
  25. charm/core/crypto/__init__.py +0 -0
  26. charm/core/crypto/cryptobase/XOR.c +80 -0
  27. charm/core/crypto/cryptobase/_counter.c +496 -0
  28. charm/core/crypto/cryptobase/_counter.h +54 -0
  29. charm/core/crypto/cryptobase/block_template.c +900 -0
  30. charm/core/crypto/cryptobase/block_template.h +69 -0
  31. charm/core/crypto/cryptobase/cryptobasemodule.c +220 -0
  32. charm/core/crypto/cryptobase/libtom/tomcrypt.h +90 -0
  33. charm/core/crypto/cryptobase/libtom/tomcrypt_argchk.h +44 -0
  34. charm/core/crypto/cryptobase/libtom/tomcrypt_cfg.h +186 -0
  35. charm/core/crypto/cryptobase/libtom/tomcrypt_cipher.h +941 -0
  36. charm/core/crypto/cryptobase/libtom/tomcrypt_custom.h +556 -0
  37. charm/core/crypto/cryptobase/libtom/tomcrypt_des.c +1912 -0
  38. charm/core/crypto/cryptobase/libtom/tomcrypt_hash.h +407 -0
  39. charm/core/crypto/cryptobase/libtom/tomcrypt_mac.h +496 -0
  40. charm/core/crypto/cryptobase/libtom/tomcrypt_macros.h +435 -0
  41. charm/core/crypto/cryptobase/libtom/tomcrypt_math.h +534 -0
  42. charm/core/crypto/cryptobase/libtom/tomcrypt_misc.h +103 -0
  43. charm/core/crypto/cryptobase/libtom/tomcrypt_pk.h +653 -0
  44. charm/core/crypto/cryptobase/libtom/tomcrypt_pkcs.h +90 -0
  45. charm/core/crypto/cryptobase/libtom/tomcrypt_prng.h +199 -0
  46. charm/core/crypto/cryptobase/stream_template.c +271 -0
  47. charm/core/crypto/cryptobase/strxor.c +229 -0
  48. charm/core/crypto/cryptobase.cpython-313-darwin.so +0 -0
  49. charm/core/engine/__init__.py +5 -0
  50. charm/core/engine/protocol.py +293 -0
  51. charm/core/engine/util.py +174 -0
  52. charm/core/math/__init__.py +0 -0
  53. charm/core/math/elliptic_curve/ecmodule.c +1986 -0
  54. charm/core/math/elliptic_curve/ecmodule.h +230 -0
  55. charm/core/math/elliptic_curve.cpython-313-darwin.so +0 -0
  56. charm/core/math/elliptic_curve.pyi +63 -0
  57. charm/core/math/integer/integermodule.c +2539 -0
  58. charm/core/math/integer/integermodule.h +145 -0
  59. charm/core/math/integer.cpython-313-darwin.so +0 -0
  60. charm/core/math/integer.pyi +76 -0
  61. charm/core/math/pairing/miracl/miracl_config.h +37 -0
  62. charm/core/math/pairing/miracl/miracl_interface.h +118 -0
  63. charm/core/math/pairing/miracl/miracl_interface2.h +126 -0
  64. charm/core/math/pairing/miracl/pairingmodule2.c +2094 -0
  65. charm/core/math/pairing/miracl/pairingmodule2.h +307 -0
  66. charm/core/math/pairing/pairingmodule.c +2230 -0
  67. charm/core/math/pairing/pairingmodule.h +241 -0
  68. charm/core/math/pairing/relic/pairingmodule3.c +1853 -0
  69. charm/core/math/pairing/relic/pairingmodule3.h +233 -0
  70. charm/core/math/pairing/relic/relic_interface.c +1337 -0
  71. charm/core/math/pairing/relic/relic_interface.h +217 -0
  72. charm/core/math/pairing/relic/test_relic.c +171 -0
  73. charm/core/math/pairing.cpython-313-darwin.so +0 -0
  74. charm/core/math/pairing.pyi +69 -0
  75. charm/core/utilities/base64.c +248 -0
  76. charm/core/utilities/base64.h +15 -0
  77. charm/schemes/__init__.py +0 -0
  78. charm/schemes/abenc/__init__.py +0 -0
  79. charm/schemes/abenc/abenc_accountability_jyjxgd20.py +647 -0
  80. charm/schemes/abenc/abenc_bsw07.py +146 -0
  81. charm/schemes/abenc/abenc_ca_cpabe_ar17.py +684 -0
  82. charm/schemes/abenc/abenc_dacmacs_yj14.py +298 -0
  83. charm/schemes/abenc/abenc_lsw08.py +159 -0
  84. charm/schemes/abenc/abenc_maabe_rw15.py +236 -0
  85. charm/schemes/abenc/abenc_maabe_yj14.py +297 -0
  86. charm/schemes/abenc/abenc_tbpre_lww14.py +309 -0
  87. charm/schemes/abenc/abenc_unmcpabe_yahk14.py +223 -0
  88. charm/schemes/abenc/abenc_waters09.py +144 -0
  89. charm/schemes/abenc/abenc_yct14.py +208 -0
  90. charm/schemes/abenc/abenc_yllc15.py +178 -0
  91. charm/schemes/abenc/ac17.py +248 -0
  92. charm/schemes/abenc/bsw07.py +141 -0
  93. charm/schemes/abenc/cgw15.py +277 -0
  94. charm/schemes/abenc/dabe_aw11.py +204 -0
  95. charm/schemes/abenc/dfa_fe12.py +144 -0
  96. charm/schemes/abenc/pk_hve08.py +179 -0
  97. charm/schemes/abenc/waters11.py +143 -0
  98. charm/schemes/aggrsign_MuSig.py +150 -0
  99. charm/schemes/aggrsign_bls.py +267 -0
  100. charm/schemes/blindsig_ps16.py +654 -0
  101. charm/schemes/chamhash_adm05.py +113 -0
  102. charm/schemes/chamhash_rsa_hw09.py +100 -0
  103. charm/schemes/commit/__init__.py +0 -0
  104. charm/schemes/commit/commit_gs08.py +77 -0
  105. charm/schemes/commit/commit_pedersen92.py +53 -0
  106. charm/schemes/encap_bchk05.py +62 -0
  107. charm/schemes/grpsig/__init__.py +0 -0
  108. charm/schemes/grpsig/groupsig_bgls04.py +114 -0
  109. charm/schemes/grpsig/groupsig_bgls04_var.py +115 -0
  110. charm/schemes/hibenc/__init__.py +0 -0
  111. charm/schemes/hibenc/hibenc_bb04.py +105 -0
  112. charm/schemes/hibenc/hibenc_lew11.py +193 -0
  113. charm/schemes/ibenc/__init__.py +0 -0
  114. charm/schemes/ibenc/clpkc_rp03.py +119 -0
  115. charm/schemes/ibenc/ibenc_CW13_z.py +168 -0
  116. charm/schemes/ibenc/ibenc_bb03.py +94 -0
  117. charm/schemes/ibenc/ibenc_bf01.py +121 -0
  118. charm/schemes/ibenc/ibenc_ckrs09.py +120 -0
  119. charm/schemes/ibenc/ibenc_cllww12_z.py +172 -0
  120. charm/schemes/ibenc/ibenc_lsw08.py +120 -0
  121. charm/schemes/ibenc/ibenc_sw05.py +238 -0
  122. charm/schemes/ibenc/ibenc_waters05.py +144 -0
  123. charm/schemes/ibenc/ibenc_waters05_z.py +164 -0
  124. charm/schemes/ibenc/ibenc_waters09.py +107 -0
  125. charm/schemes/ibenc/ibenc_waters09_z.py +147 -0
  126. charm/schemes/joye_scheme.py +106 -0
  127. charm/schemes/lem_scheme.py +207 -0
  128. charm/schemes/pk_fre_ccv11.py +107 -0
  129. charm/schemes/pk_vrf.py +127 -0
  130. charm/schemes/pkenc/__init__.py +0 -0
  131. charm/schemes/pkenc/pkenc_cs98.py +108 -0
  132. charm/schemes/pkenc/pkenc_elgamal85.py +122 -0
  133. charm/schemes/pkenc/pkenc_gm82.py +98 -0
  134. charm/schemes/pkenc/pkenc_paillier99.py +118 -0
  135. charm/schemes/pkenc/pkenc_rabin.py +254 -0
  136. charm/schemes/pkenc/pkenc_rsa.py +186 -0
  137. charm/schemes/pksig/__init__.py +0 -0
  138. charm/schemes/pksig/pksig_CW13_z.py +135 -0
  139. charm/schemes/pksig/pksig_bls04.py +87 -0
  140. charm/schemes/pksig/pksig_boyen.py +156 -0
  141. charm/schemes/pksig/pksig_chch.py +97 -0
  142. charm/schemes/pksig/pksig_chp.py +70 -0
  143. charm/schemes/pksig/pksig_cl03.py +150 -0
  144. charm/schemes/pksig/pksig_cl04.py +87 -0
  145. charm/schemes/pksig/pksig_cllww12_z.py +142 -0
  146. charm/schemes/pksig/pksig_cyh.py +132 -0
  147. charm/schemes/pksig/pksig_dsa.py +76 -0
  148. charm/schemes/pksig/pksig_ecdsa.py +71 -0
  149. charm/schemes/pksig/pksig_hess.py +104 -0
  150. charm/schemes/pksig/pksig_hw.py +110 -0
  151. charm/schemes/pksig/pksig_lamport.py +63 -0
  152. charm/schemes/pksig/pksig_ps01.py +135 -0
  153. charm/schemes/pksig/pksig_ps02.py +124 -0
  154. charm/schemes/pksig/pksig_ps03.py +119 -0
  155. charm/schemes/pksig/pksig_rsa_hw09.py +206 -0
  156. charm/schemes/pksig/pksig_schnorr91.py +77 -0
  157. charm/schemes/pksig/pksig_waters.py +115 -0
  158. charm/schemes/pksig/pksig_waters05.py +121 -0
  159. charm/schemes/pksig/pksig_waters09.py +121 -0
  160. charm/schemes/pre_mg07.py +150 -0
  161. charm/schemes/prenc/pre_afgh06.py +126 -0
  162. charm/schemes/prenc/pre_bbs98.py +123 -0
  163. charm/schemes/prenc/pre_nal16.py +216 -0
  164. charm/schemes/protocol_a01.py +272 -0
  165. charm/schemes/protocol_ao00.py +215 -0
  166. charm/schemes/protocol_cns07.py +274 -0
  167. charm/schemes/protocol_schnorr91.py +125 -0
  168. charm/schemes/sigma1.py +64 -0
  169. charm/schemes/sigma2.py +129 -0
  170. charm/schemes/sigma3.py +126 -0
  171. charm/schemes/threshold/__init__.py +59 -0
  172. charm/schemes/threshold/dkls23_dkg.py +556 -0
  173. charm/schemes/threshold/dkls23_presign.py +1089 -0
  174. charm/schemes/threshold/dkls23_sign.py +761 -0
  175. charm/schemes/threshold/xrpl_wallet.py +967 -0
  176. charm/test/__init__.py +0 -0
  177. charm/test/adapters/__init__.py +0 -0
  178. charm/test/adapters/abenc_adapt_hybrid_test.py +29 -0
  179. charm/test/adapters/dabenc_adapt_hybrid_test.py +56 -0
  180. charm/test/adapters/ibenc_adapt_hybrid_test.py +36 -0
  181. charm/test/adapters/ibenc_adapt_identityhash_test.py +32 -0
  182. charm/test/adapters/kpabenc_adapt_hybrid_test.py +30 -0
  183. charm/test/benchmark/abenc_yllc15_bench.py +92 -0
  184. charm/test/benchmark/benchmark_test.py +148 -0
  185. charm/test/benchmark_threshold.py +260 -0
  186. charm/test/conftest.py +38 -0
  187. charm/test/fuzz/__init__.py +1 -0
  188. charm/test/fuzz/conftest.py +5 -0
  189. charm/test/fuzz/fuzz_policy_parser.py +76 -0
  190. charm/test/fuzz/fuzz_serialization.py +83 -0
  191. charm/test/schemes/__init__.py +0 -0
  192. charm/test/schemes/abenc/__init__.py +0 -0
  193. charm/test/schemes/abenc/abenc_bsw07_test.py +39 -0
  194. charm/test/schemes/abenc/abenc_dacmacs_yj14_test.py +16 -0
  195. charm/test/schemes/abenc/abenc_lsw08_test.py +33 -0
  196. charm/test/schemes/abenc/abenc_maabe_yj14_test.py +16 -0
  197. charm/test/schemes/abenc/abenc_tbpre_lww14_test.py +16 -0
  198. charm/test/schemes/abenc/abenc_waters09_test.py +38 -0
  199. charm/test/schemes/abenc/abenc_yllc15_test.py +74 -0
  200. charm/test/schemes/chamhash_adm05_test.py +31 -0
  201. charm/test/schemes/chamhash_rsa_hw09_test.py +29 -0
  202. charm/test/schemes/commit/__init__.py +0 -0
  203. charm/test/schemes/commit/commit_gs08_test.py +24 -0
  204. charm/test/schemes/commit/commit_pedersen92_test.py +26 -0
  205. charm/test/schemes/dabe_aw11_test.py +45 -0
  206. charm/test/schemes/encap_bchk05_test.py +21 -0
  207. charm/test/schemes/grpsig/__init__.py +0 -0
  208. charm/test/schemes/grpsig/groupsig_bgls04_test.py +35 -0
  209. charm/test/schemes/grpsig/groupsig_bgls04_var_test.py +39 -0
  210. charm/test/schemes/hibenc/__init__.py +0 -0
  211. charm/test/schemes/hibenc/hibenc_bb04_test.py +28 -0
  212. charm/test/schemes/ibenc/__init__.py +0 -0
  213. charm/test/schemes/ibenc/ibenc_bb03_test.py +26 -0
  214. charm/test/schemes/ibenc/ibenc_bf01_test.py +24 -0
  215. charm/test/schemes/ibenc/ibenc_ckrs09_test.py +25 -0
  216. charm/test/schemes/ibenc/ibenc_lsw08_test.py +31 -0
  217. charm/test/schemes/ibenc/ibenc_sw05_test.py +32 -0
  218. charm/test/schemes/ibenc/ibenc_waters05_test.py +31 -0
  219. charm/test/schemes/ibenc/ibenc_waters09_test.py +27 -0
  220. charm/test/schemes/pk_vrf_test.py +29 -0
  221. charm/test/schemes/pkenc/__init__.py +0 -0
  222. charm/test/schemes/pkenc_test.py +255 -0
  223. charm/test/schemes/pksig/__init__.py +0 -0
  224. charm/test/schemes/pksig_test.py +376 -0
  225. charm/test/schemes/rsa_alg_test.py +340 -0
  226. charm/test/schemes/threshold_test.py +1792 -0
  227. charm/test/serialize/__init__.py +0 -0
  228. charm/test/serialize/serialize_test.py +40 -0
  229. charm/test/toolbox/__init__.py +0 -0
  230. charm/test/toolbox/conversion_test.py +30 -0
  231. charm/test/toolbox/ecgroup_test.py +53 -0
  232. charm/test/toolbox/integer_arithmetic_test.py +441 -0
  233. charm/test/toolbox/paddingschemes_test.py +238 -0
  234. charm/test/toolbox/policy_parser_stress_test.py +969 -0
  235. charm/test/toolbox/secretshare_test.py +28 -0
  236. charm/test/toolbox/symcrypto_test.py +108 -0
  237. charm/test/toolbox/test_policy_expression.py +16 -0
  238. charm/test/vectors/__init__.py +1 -0
  239. charm/test/vectors/test_bls_vectors.py +289 -0
  240. charm/test/vectors/test_pedersen_vectors.py +315 -0
  241. charm/test/vectors/test_schnorr_vectors.py +368 -0
  242. charm/test/zkp_compiler/__init__.py +9 -0
  243. charm/test/zkp_compiler/benchmark_zkp.py +258 -0
  244. charm/test/zkp_compiler/test_and_proof.py +240 -0
  245. charm/test/zkp_compiler/test_batch_verify.py +248 -0
  246. charm/test/zkp_compiler/test_dleq_proof.py +264 -0
  247. charm/test/zkp_compiler/test_or_proof.py +231 -0
  248. charm/test/zkp_compiler/test_proof_serialization.py +121 -0
  249. charm/test/zkp_compiler/test_range_proof.py +241 -0
  250. charm/test/zkp_compiler/test_representation_proof.py +325 -0
  251. charm/test/zkp_compiler/test_schnorr_proof.py +221 -0
  252. charm/test/zkp_compiler/test_thread_safety.py +169 -0
  253. charm/test/zkp_compiler/test_zkp_parser.py +139 -0
  254. charm/toolbox/ABEnc.py +26 -0
  255. charm/toolbox/ABEncMultiAuth.py +66 -0
  256. charm/toolbox/ABEnumeric.py +800 -0
  257. charm/toolbox/Commit.py +24 -0
  258. charm/toolbox/DFA.py +89 -0
  259. charm/toolbox/FSA.py +1254 -0
  260. charm/toolbox/Hash.py +39 -0
  261. charm/toolbox/IBEnc.py +62 -0
  262. charm/toolbox/IBSig.py +64 -0
  263. charm/toolbox/PKEnc.py +66 -0
  264. charm/toolbox/PKSig.py +56 -0
  265. charm/toolbox/PREnc.py +32 -0
  266. charm/toolbox/ZKProof.py +289 -0
  267. charm/toolbox/__init__.py +0 -0
  268. charm/toolbox/bitstring.py +49 -0
  269. charm/toolbox/broadcast.py +220 -0
  270. charm/toolbox/conversion.py +100 -0
  271. charm/toolbox/eccurve.py +149 -0
  272. charm/toolbox/ecgroup.py +143 -0
  273. charm/toolbox/enum.py +60 -0
  274. charm/toolbox/hash_module.py +91 -0
  275. charm/toolbox/integergroup.py +323 -0
  276. charm/toolbox/iterate.py +22 -0
  277. charm/toolbox/matrixops.py +76 -0
  278. charm/toolbox/mpc_utils.py +296 -0
  279. charm/toolbox/msp.py +175 -0
  280. charm/toolbox/mta.py +985 -0
  281. charm/toolbox/node.py +120 -0
  282. charm/toolbox/ot/__init__.py +22 -0
  283. charm/toolbox/ot/base_ot.py +374 -0
  284. charm/toolbox/ot/dpf.py +642 -0
  285. charm/toolbox/ot/mpfss.py +228 -0
  286. charm/toolbox/ot/ot_extension.py +589 -0
  287. charm/toolbox/ot/silent_ot.py +378 -0
  288. charm/toolbox/paddingschemes.py +423 -0
  289. charm/toolbox/paddingschemes_test.py +238 -0
  290. charm/toolbox/pairingcurves.py +85 -0
  291. charm/toolbox/pairinggroup.py +186 -0
  292. charm/toolbox/policy_expression_spec.py +70 -0
  293. charm/toolbox/policytree.py +189 -0
  294. charm/toolbox/reCompiler.py +346 -0
  295. charm/toolbox/redundancyschemes.py +65 -0
  296. charm/toolbox/schemebase.py +188 -0
  297. charm/toolbox/secretshare.py +104 -0
  298. charm/toolbox/secretutil.py +174 -0
  299. charm/toolbox/securerandom.py +73 -0
  300. charm/toolbox/sigmaprotocol.py +46 -0
  301. charm/toolbox/specialprimes.py +45 -0
  302. charm/toolbox/symcrypto.py +279 -0
  303. charm/toolbox/threshold_sharing.py +553 -0
  304. charm/toolbox/xmlserialize.py +94 -0
  305. charm/toolbox/zknode.py +105 -0
  306. charm/zkp_compiler/__init__.py +89 -0
  307. charm/zkp_compiler/and_proof.py +460 -0
  308. charm/zkp_compiler/batch_verify.py +324 -0
  309. charm/zkp_compiler/dleq_proof.py +423 -0
  310. charm/zkp_compiler/or_proof.py +305 -0
  311. charm/zkp_compiler/range_proof.py +417 -0
  312. charm/zkp_compiler/representation_proof.py +466 -0
  313. charm/zkp_compiler/schnorr_proof.py +273 -0
  314. charm/zkp_compiler/thread_safe.py +150 -0
  315. charm/zkp_compiler/zk_demo.py +489 -0
  316. charm/zkp_compiler/zkp_factory.py +330 -0
  317. charm/zkp_compiler/zkp_generator.py +370 -0
  318. charm/zkp_compiler/zkparser.py +269 -0
  319. charm_crypto_framework-0.61.1.dist-info/METADATA +337 -0
  320. charm_crypto_framework-0.61.1.dist-info/RECORD +323 -0
  321. charm_crypto_framework-0.61.1.dist-info/WHEEL +5 -0
  322. charm_crypto_framework-0.61.1.dist-info/licenses/LICENSE.txt +165 -0
  323. charm_crypto_framework-0.61.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,967 @@
1
+ """
2
+ XRPL Threshold Wallet Integration
3
+
4
+ This module provides XRPL (XRP Ledger) wallet functionality using the DKLS23
5
+ threshold ECDSA implementation. It enables creating threshold-controlled XRPL
6
+ wallets where t-of-n parties must cooperate to sign transactions.
7
+
8
+ XRPL Compatibility:
9
+ - Uses secp256k1 curve (same as XRPL)
10
+ - 33-byte compressed public key format
11
+ - DER-encoded signatures
12
+ - Standard XRPL address derivation (SHA-256 → RIPEMD-160 → base58)
13
+
14
+ Example
15
+ -------
16
+ >>> from charm.toolbox.eccurve import secp256k1
17
+ >>> from charm.toolbox.ecgroup import ECGroup
18
+ >>> from charm.schemes.threshold.dkls23_sign import DKLS23
19
+ >>> from charm.schemes.threshold.xrpl_wallet import XRPLThresholdWallet
20
+ >>>
21
+ >>> # Create 2-of-3 threshold ECDSA
22
+ >>> group = ECGroup(secp256k1)
23
+ >>> dkls = DKLS23(group, threshold=2, num_parties=3)
24
+ >>> g = group.random(G)
25
+ >>>
26
+ >>> # Generate distributed keys
27
+ >>> key_shares, public_key = dkls.distributed_keygen(g)
28
+ >>>
29
+ >>> # Create XRPL wallet from threshold public key
30
+ >>> wallet = XRPLThresholdWallet(group, public_key)
31
+ >>> address = wallet.get_classic_address()
32
+ >>> print(f"XRPL Address: {address}") # doctest: +SKIP
33
+
34
+ References
35
+ ----------
36
+ - XRPL Cryptographic Keys: https://xrpl.org/docs/concepts/accounts/cryptographic-keys
37
+ - XRPL Address Encoding: https://xrpl.org/docs/concepts/accounts/addresses
38
+ - DKLS23: "Two-Round Threshold ECDSA from ECDSA Assumptions"
39
+
40
+ Note
41
+ ----
42
+ This module provides cryptographic primitives only. For full XRPL integration,
43
+ you will need the xrpl-py library for transaction serialization and network
44
+ communication. See XRPL_GAPS.md for details on missing functionality.
45
+ """
46
+
47
+ import hashlib
48
+ import base64
49
+ from typing import Optional, Tuple
50
+
51
+ from charm.core.math.elliptic_curve import getGenerator
52
+
53
+
54
+ def get_secp256k1_generator(group):
55
+ """
56
+ Get the standard secp256k1 generator point.
57
+
58
+ This returns the fixed generator point specified in the secp256k1 standard,
59
+ NOT a random point. This is required for ECDSA signatures that need to be
60
+ verified by external systems like XRPL.
61
+
62
+ Args:
63
+ group: ECGroup instance initialized with secp256k1
64
+
65
+ Returns:
66
+ The standard secp256k1 generator point G
67
+
68
+ Example:
69
+ >>> from charm.toolbox.eccurve import secp256k1
70
+ >>> from charm.toolbox.ecgroup import ECGroup
71
+ >>> group = ECGroup(secp256k1)
72
+ >>> g = get_secp256k1_generator(group)
73
+ >>> # g is now the standard generator, not a random point
74
+ """
75
+ return getGenerator(group.ec_group)
76
+
77
+ from charm.toolbox.ecgroup import ECGroup
78
+ from charm.core.math.elliptic_curve import ec_element, serialize, ZR, G
79
+
80
+ # XRPL base58 alphabet (different from Bitcoin's base58)
81
+ XRPL_ALPHABET = b'rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz'
82
+
83
+
84
+ def _base58_encode(data: bytes) -> str:
85
+ """Encode bytes to XRPL base58 format."""
86
+ # Convert bytes to integer
87
+ n = int.from_bytes(data, 'big')
88
+
89
+ # Convert to base58
90
+ result = []
91
+ while n > 0:
92
+ n, remainder = divmod(n, 58)
93
+ result.append(XRPL_ALPHABET[remainder:remainder+1])
94
+
95
+ # Add leading zeros
96
+ for byte in data:
97
+ if byte == 0:
98
+ result.append(XRPL_ALPHABET[0:1])
99
+ else:
100
+ break
101
+
102
+ return b''.join(reversed(result)).decode('ascii')
103
+
104
+
105
+ def _sha256(data: bytes) -> bytes:
106
+ """Compute SHA-256 hash."""
107
+ return hashlib.sha256(data).digest()
108
+
109
+
110
+ def _ripemd160(data: bytes) -> bytes:
111
+ """Compute RIPEMD-160 hash."""
112
+ h = hashlib.new('ripemd160')
113
+ h.update(data)
114
+ return h.digest()
115
+
116
+
117
+ def _double_sha256(data: bytes) -> bytes:
118
+ """Compute double SHA-256 hash (for checksum)."""
119
+ return _sha256(_sha256(data))
120
+
121
+
122
+ def get_compressed_public_key(group: ECGroup, public_key: ec_element) -> bytes:
123
+ """
124
+ Get 33-byte compressed public key from EC point.
125
+
126
+ XRPL requires compressed secp256k1 public keys (33 bytes):
127
+ - 0x02 prefix if Y coordinate is even
128
+ - 0x03 prefix if Y coordinate is odd
129
+ - Followed by 32-byte X coordinate
130
+
131
+ Args:
132
+ group: The EC group (should be secp256k1)
133
+ public_key: The EC point representing the public key
134
+
135
+ Returns:
136
+ 33-byte compressed public key
137
+ """
138
+ # Charm's serialize uses compressed format, but wraps in base64 with type prefix
139
+ # Format is "type:base64_data" where type=1 for G (group element)
140
+ serialized = serialize(public_key)
141
+
142
+ # Parse the Charm format: "1:base64data"
143
+ if isinstance(serialized, bytes):
144
+ serialized = serialized.decode('ascii')
145
+
146
+ parts = serialized.split(':')
147
+ if len(parts) != 2:
148
+ raise ValueError(f"Unexpected serialization format: {serialized}")
149
+
150
+ type_id, b64_data = parts
151
+ if type_id != '1':
152
+ raise ValueError(f"Expected type 1 (group element), got {type_id}")
153
+
154
+ # Decode base64 to get raw compressed point
155
+ compressed = base64.b64decode(b64_data)
156
+
157
+ if len(compressed) != 33:
158
+ raise ValueError(f"Expected 33-byte compressed key, got {len(compressed)} bytes")
159
+
160
+ return compressed
161
+
162
+
163
+ def derive_account_id(compressed_pubkey: bytes) -> bytes:
164
+ """
165
+ Derive XRPL Account ID from compressed public key.
166
+
167
+ Account ID = RIPEMD160(SHA256(public_key))
168
+
169
+ Args:
170
+ compressed_pubkey: 33-byte compressed secp256k1 public key
171
+
172
+ Returns:
173
+ 20-byte Account ID
174
+ """
175
+ if len(compressed_pubkey) != 33:
176
+ raise ValueError(f"Expected 33-byte compressed public key, got {len(compressed_pubkey)}")
177
+
178
+ sha256_hash = _sha256(compressed_pubkey)
179
+ account_id = _ripemd160(sha256_hash)
180
+
181
+ return account_id
182
+
183
+
184
+ def encode_classic_address(account_id: bytes) -> str:
185
+ """
186
+ Encode Account ID as XRPL classic address.
187
+
188
+ Classic address = base58(0x00 + account_id + checksum)
189
+
190
+ Args:
191
+ account_id: 20-byte Account ID
192
+
193
+ Returns:
194
+ Classic XRPL address (starts with 'r')
195
+ """
196
+ if len(account_id) != 20:
197
+ raise ValueError(f"Expected 20-byte account ID, got {len(account_id)}")
198
+
199
+ # Prefix with 0x00 for account address
200
+ payload = b'\x00' + account_id
201
+
202
+ # Calculate checksum (first 4 bytes of double SHA-256)
203
+ checksum = _double_sha256(payload)[:4]
204
+
205
+ # Encode with checksum
206
+ return _base58_encode(payload + checksum)
207
+
208
+
209
+ class XRPLThresholdWallet:
210
+ """
211
+ XRPL wallet using threshold ECDSA for signing.
212
+
213
+ This class wraps a threshold-generated public key and provides XRPL-specific
214
+ functionality like address derivation and transaction signing coordination.
215
+
216
+ Example
217
+ -------
218
+ >>> from charm.toolbox.eccurve import secp256k1
219
+ >>> from charm.toolbox.ecgroup import ECGroup
220
+ >>> from charm.schemes.threshold.dkls23_sign import DKLS23
221
+ >>> group = ECGroup(secp256k1)
222
+ >>> dkls = DKLS23(group, threshold=2, num_parties=3)
223
+ >>> g = group.random(G)
224
+ >>> key_shares, public_key = dkls.distributed_keygen(g)
225
+ >>> wallet = XRPLThresholdWallet(group, public_key)
226
+ >>> len(wallet.get_compressed_public_key()) == 33
227
+ True
228
+ >>> wallet.get_classic_address().startswith('r')
229
+ True
230
+ """
231
+
232
+ def __init__(self, group: ECGroup, public_key: ec_element):
233
+ """
234
+ Initialize XRPL threshold wallet.
235
+
236
+ Args:
237
+ group: EC group (should be secp256k1 for XRPL)
238
+ public_key: Combined threshold public key from DKG
239
+ """
240
+ self.group = group
241
+ self.public_key = public_key
242
+ self._compressed_pubkey = None
243
+ self._account_id = None
244
+ self._classic_address = None
245
+
246
+ def get_compressed_public_key(self) -> bytes:
247
+ """Get 33-byte compressed public key."""
248
+ if self._compressed_pubkey is None:
249
+ self._compressed_pubkey = get_compressed_public_key(self.group, self.public_key)
250
+ return self._compressed_pubkey
251
+
252
+ def get_account_id(self) -> bytes:
253
+ """Get 20-byte XRPL Account ID."""
254
+ if self._account_id is None:
255
+ self._account_id = derive_account_id(self.get_compressed_public_key())
256
+ return self._account_id
257
+
258
+ def get_classic_address(self) -> str:
259
+ """Get XRPL classic address (starts with 'r')."""
260
+ if self._classic_address is None:
261
+ self._classic_address = encode_classic_address(self.get_account_id())
262
+ return self._classic_address
263
+
264
+ def get_account_id_hex(self) -> str:
265
+ """Get Account ID as hex string."""
266
+ return self.get_account_id().hex().upper()
267
+
268
+ def get_public_key_hex(self) -> str:
269
+ """Get compressed public key as hex string."""
270
+ return self.get_compressed_public_key().hex().upper()
271
+
272
+ def get_x_address(self, tag: Optional[int] = None,
273
+ is_testnet: bool = False) -> str:
274
+ """
275
+ Get X-address for this wallet.
276
+
277
+ X-addresses encode the destination tag into the address,
278
+ which can help prevent forgotten destination tags.
279
+
280
+ Args:
281
+ tag: Optional destination tag (0-4294967295)
282
+ is_testnet: True for testnet, False for mainnet
283
+
284
+ Returns:
285
+ X-address string
286
+
287
+ Raises:
288
+ ImportError: If xrpl-py is not installed
289
+ """
290
+ return get_x_address(self.get_classic_address(), tag=tag,
291
+ is_testnet=is_testnet)
292
+
293
+
294
+ def sign_xrpl_transaction_hash(
295
+ dkls,
296
+ participants: list,
297
+ presignatures: dict,
298
+ key_shares: dict,
299
+ tx_hash: bytes,
300
+ generator
301
+ ) -> bytes:
302
+ """
303
+ Sign an XRPL transaction hash using threshold ECDSA.
304
+
305
+ This function takes a pre-computed transaction hash and produces a
306
+ DER-encoded signature suitable for XRPL transaction submission.
307
+
308
+ Args:
309
+ dkls: DKLS23 instance
310
+ participants: List of participating party IDs
311
+ presignatures: Presignatures from presign()
312
+ key_shares: Key shares from distributed_keygen()
313
+ tx_hash: 32-byte transaction hash (from XRPL transaction serialization)
314
+ generator: Generator point used in key generation
315
+
316
+ Returns:
317
+ DER-encoded signature bytes
318
+
319
+ Raises:
320
+ ValueError: If tx_hash is not 32 bytes
321
+
322
+ Example
323
+ -------
324
+ >>> from charm.toolbox.eccurve import secp256k1
325
+ >>> from charm.toolbox.ecgroup import ECGroup
326
+ >>> from charm.schemes.threshold.dkls23_sign import DKLS23
327
+ >>> group = ECGroup(secp256k1)
328
+ >>> dkls = DKLS23(group, threshold=2, num_parties=3)
329
+ >>> g = group.random(G)
330
+ >>> key_shares, public_key = dkls.distributed_keygen(g)
331
+ >>> presigs = dkls.presign([1, 2], key_shares, g)
332
+ >>> # Simulate a transaction hash (normally from xrpl-py)
333
+ >>> tx_hash = b'\\x00' * 32
334
+ >>> der_sig = sign_xrpl_transaction_hash(dkls, [1, 2], presigs, key_shares, tx_hash, g)
335
+ >>> der_sig[0] == 0x30 # DER SEQUENCE tag
336
+ True
337
+ """
338
+ if len(tx_hash) != 32:
339
+ raise ValueError(f"Transaction hash must be 32 bytes, got {len(tx_hash)}")
340
+
341
+ # Sign the hash using threshold ECDSA
342
+ # Use prehashed=True since XRPL provides its own signing hash (SHA-512 truncated to 32 bytes)
343
+ signature = dkls.sign(participants, presignatures, key_shares, tx_hash, generator, prehashed=True)
344
+
345
+ # Convert to DER encoding for XRPL
346
+ return signature.to_der()
347
+
348
+
349
+ def format_xrpl_signature(der_signature: bytes, public_key_hex: str) -> dict:
350
+ """
351
+ Format signature for XRPL transaction submission.
352
+
353
+ Returns the signature and public key in the format expected by XRPL.
354
+
355
+ Args:
356
+ der_signature: DER-encoded signature from sign_xrpl_transaction_hash()
357
+ public_key_hex: Hex-encoded compressed public key
358
+
359
+ Returns:
360
+ Dict with 'TxnSignature' and 'SigningPubKey' fields
361
+ """
362
+ return {
363
+ 'TxnSignature': der_signature.hex().upper(),
364
+ 'SigningPubKey': public_key_hex
365
+ }
366
+
367
+
368
+ # =============================================================================
369
+ # Full XRPL Integration (requires xrpl-py)
370
+ # =============================================================================
371
+
372
+ def _check_xrpl_py():
373
+ """Check if xrpl-py is available."""
374
+ try:
375
+ import xrpl
376
+ return True
377
+ except ImportError:
378
+ return False
379
+
380
+
381
+ def get_x_address(classic_address: str, tag: Optional[int] = None,
382
+ is_testnet: bool = False) -> str:
383
+ """
384
+ Convert classic address to X-address format.
385
+
386
+ X-addresses encode the destination tag into the address itself,
387
+ reducing the risk of forgetting to include it.
388
+
389
+ Args:
390
+ classic_address: Classic XRPL address (starts with 'r')
391
+ tag: Optional destination tag (0-4294967295)
392
+ is_testnet: True for testnet, False for mainnet
393
+
394
+ Returns:
395
+ X-address string
396
+
397
+ Raises:
398
+ ImportError: If xrpl-py is not installed
399
+
400
+ Example:
401
+ >>> get_x_address('rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh') # doctest: +SKIP
402
+ 'XVPcpSm47b1CZkf5AkKM9a84dQHe3m4sBhsrA4XtnBECTAc'
403
+ """
404
+ try:
405
+ from xrpl.core.addresscodec import classic_address_to_xaddress
406
+ except ImportError:
407
+ raise ImportError(
408
+ "xrpl-py is required for X-address support. "
409
+ "Install with: pip install xrpl-py"
410
+ )
411
+
412
+ return classic_address_to_xaddress(classic_address, tag=tag,
413
+ is_test_network=is_testnet)
414
+
415
+
416
+ def decode_x_address(x_address: str) -> Tuple[str, Optional[int], bool]:
417
+ """
418
+ Decode X-address to classic address, tag, and network.
419
+
420
+ Args:
421
+ x_address: X-address string
422
+
423
+ Returns:
424
+ Tuple of (classic_address, tag, is_testnet)
425
+
426
+ Raises:
427
+ ImportError: If xrpl-py is not installed
428
+ """
429
+ try:
430
+ from xrpl.core.addresscodec import xaddress_to_classic_address
431
+ except ImportError:
432
+ raise ImportError(
433
+ "xrpl-py is required for X-address support. "
434
+ "Install with: pip install xrpl-py"
435
+ )
436
+
437
+ return xaddress_to_classic_address(x_address)
438
+
439
+
440
+ def compute_signing_hash(transaction) -> bytes:
441
+ """
442
+ Compute the signing hash for an XRPL transaction.
443
+
444
+ Takes an xrpl-py transaction model or dict and returns the 32-byte
445
+ hash that should be signed.
446
+
447
+ Args:
448
+ transaction: xrpl.models.Transaction or dict with transaction fields
449
+
450
+ Returns:
451
+ 32-byte signing hash
452
+
453
+ Raises:
454
+ ImportError: If xrpl-py is not installed
455
+
456
+ Example:
457
+ >>> from xrpl.models import Payment # doctest: +SKIP
458
+ >>> tx = Payment(account='r...', destination='r...', amount='1000000')
459
+ >>> tx_hash = compute_signing_hash(tx) # doctest: +SKIP
460
+ >>> len(tx_hash) == 32 # doctest: +SKIP
461
+ True
462
+ """
463
+ try:
464
+ from xrpl.transaction import transaction_json_to_binary_codec_form
465
+ from xrpl.core.binarycodec import encode_for_signing
466
+ except ImportError:
467
+ raise ImportError(
468
+ "xrpl-py is required for transaction serialization. "
469
+ "Install with: pip install xrpl-py"
470
+ )
471
+
472
+ # Get dict representation
473
+ if hasattr(transaction, 'to_dict'):
474
+ tx_dict = transaction.to_dict()
475
+ else:
476
+ tx_dict = dict(transaction)
477
+
478
+ # Convert to binary codec form (lowercase keys -> CamelCase)
479
+ binary_form = transaction_json_to_binary_codec_form(tx_dict)
480
+
481
+ # Encode for signing
482
+ blob = encode_for_signing(binary_form)
483
+
484
+ # XRPL signing hash = SHA-512 first 32 bytes
485
+ return hashlib.sha512(bytes.fromhex(blob)).digest()[:32]
486
+
487
+
488
+ def sign_xrpl_transaction(
489
+ dkls,
490
+ wallet: 'XRPLThresholdWallet',
491
+ participants: list,
492
+ presignatures: dict,
493
+ key_shares: dict,
494
+ transaction,
495
+ generator
496
+ ) -> str:
497
+ """
498
+ Sign an XRPL transaction and return the signed transaction blob.
499
+
500
+ This is the main end-to-end signing function that takes a transaction
501
+ model, computes the signing hash, signs it with threshold ECDSA, and
502
+ returns the complete signed transaction ready for submission.
503
+
504
+ Args:
505
+ dkls: DKLS23 instance
506
+ wallet: XRPLThresholdWallet for this account
507
+ participants: List of participating party IDs
508
+ presignatures: Presignatures from presign()
509
+ key_shares: Key shares from distributed_keygen()
510
+ transaction: xrpl.models.Transaction or dict
511
+ generator: Generator point used in key generation
512
+
513
+ Returns:
514
+ Hex-encoded signed transaction blob ready for submission
515
+
516
+ Raises:
517
+ ImportError: If xrpl-py is not installed
518
+
519
+ Example:
520
+ >>> # Full signing example (requires xrpl-py) # doctest: +SKIP
521
+ >>> from xrpl.models import Payment
522
+ >>> tx = Payment(
523
+ ... account=wallet.get_classic_address(),
524
+ ... destination='rDestination...',
525
+ ... amount='1000000',
526
+ ... fee='12',
527
+ ... sequence=1
528
+ ... )
529
+ >>> signed_blob = sign_xrpl_transaction(
530
+ ... dkls, wallet, [1, 2], presigs, key_shares, tx, g
531
+ ... )
532
+ """
533
+ try:
534
+ from xrpl.transaction import transaction_json_to_binary_codec_form
535
+ from xrpl.core.binarycodec import encode_for_signing, encode
536
+ except ImportError:
537
+ raise ImportError(
538
+ "xrpl-py is required for transaction signing. "
539
+ "Install with: pip install xrpl-py"
540
+ )
541
+
542
+ # Get dict representation
543
+ if hasattr(transaction, 'to_dict'):
544
+ tx_dict = transaction.to_dict()
545
+ else:
546
+ tx_dict = dict(transaction)
547
+
548
+ # Convert to binary codec form
549
+ binary_form = transaction_json_to_binary_codec_form(tx_dict)
550
+
551
+ # Add signing public key
552
+ binary_form['SigningPubKey'] = wallet.get_public_key_hex()
553
+
554
+ # Compute signing hash
555
+ blob = encode_for_signing(binary_form)
556
+ tx_hash = hashlib.sha512(bytes.fromhex(blob)).digest()[:32]
557
+
558
+ # Sign with threshold ECDSA
559
+ der_sig = sign_xrpl_transaction_hash(
560
+ dkls, participants, presignatures, key_shares, tx_hash, generator
561
+ )
562
+
563
+ # Add signature to transaction
564
+ binary_form['TxnSignature'] = der_sig.hex().upper()
565
+
566
+ # Encode final signed transaction
567
+ return encode(binary_form)
568
+
569
+
570
+
571
+ class XRPLClient:
572
+ """
573
+ Client for XRPL network communication.
574
+
575
+ Provides methods for querying account information and submitting
576
+ transactions to the XRP Ledger network.
577
+
578
+ Example:
579
+ >>> client = XRPLClient() # Mainnet # doctest: +SKIP
580
+ >>> client = XRPLClient(url='https://s.altnet.rippletest.net:51234/') # Testnet
581
+ >>> seq = client.get_account_sequence('rAddress...') # doctest: +SKIP
582
+ """
583
+
584
+ # Common XRPL network URLs
585
+ MAINNET_URL = 'https://xrplcluster.com/'
586
+ TESTNET_URL = 'https://s.altnet.rippletest.net:51234/'
587
+ DEVNET_URL = 'https://s.devnet.rippletest.net:51234/'
588
+
589
+ def __init__(self, url: Optional[str] = None, is_testnet: bool = False):
590
+ """
591
+ Initialize XRPL client.
592
+
593
+ Args:
594
+ url: JSON-RPC URL for XRPL node. If None, uses mainnet/testnet default.
595
+ is_testnet: If True and url is None, use testnet URL
596
+ """
597
+ try:
598
+ from xrpl.clients import JsonRpcClient
599
+ except ImportError:
600
+ raise ImportError(
601
+ "xrpl-py is required for network communication. "
602
+ "Install with: pip install xrpl-py"
603
+ )
604
+
605
+ if url is None:
606
+ url = self.TESTNET_URL if is_testnet else self.MAINNET_URL
607
+
608
+ self.url = url
609
+ self.is_testnet = is_testnet
610
+ self._client = JsonRpcClient(url)
611
+
612
+ @property
613
+ def client(self):
614
+ """Get the underlying xrpl-py JsonRpcClient."""
615
+ return self._client
616
+
617
+ def get_account_sequence(self, address: str) -> int:
618
+ """
619
+ Get the next valid sequence number for an account.
620
+
621
+ Args:
622
+ address: XRPL account address (classic or X-address)
623
+
624
+ Returns:
625
+ Next valid sequence number for transactions
626
+
627
+ Raises:
628
+ XRPLRequestFailureException: If the account doesn't exist
629
+ """
630
+ from xrpl.account import get_next_valid_seq_number
631
+ from xrpl.core.addresscodec import is_valid_xaddress, xaddress_to_classic_address
632
+
633
+ # Convert X-address to classic if needed
634
+ if is_valid_xaddress(address):
635
+ address, _, _ = xaddress_to_classic_address(address)
636
+
637
+ return get_next_valid_seq_number(address, self._client)
638
+
639
+ def get_balance(self, address: str) -> int:
640
+ """
641
+ Get the XRP balance for an account in drops.
642
+
643
+ Args:
644
+ address: XRPL account address
645
+
646
+ Returns:
647
+ Balance in drops (1 XRP = 1,000,000 drops)
648
+ """
649
+ from xrpl.account import get_balance
650
+ from xrpl.core.addresscodec import is_valid_xaddress, xaddress_to_classic_address
651
+
652
+ # Convert X-address to classic if needed
653
+ if is_valid_xaddress(address):
654
+ address, _, _ = xaddress_to_classic_address(address)
655
+
656
+ return get_balance(address, self._client)
657
+
658
+ def does_account_exist(self, address: str) -> bool:
659
+ """
660
+ Check if an account exists and is funded on the ledger.
661
+
662
+ Args:
663
+ address: XRPL account address
664
+
665
+ Returns:
666
+ True if account exists, False otherwise
667
+ """
668
+ from xrpl.account import does_account_exist
669
+ from xrpl.core.addresscodec import is_valid_xaddress, xaddress_to_classic_address
670
+
671
+ if is_valid_xaddress(address):
672
+ address, _, _ = xaddress_to_classic_address(address)
673
+
674
+ return does_account_exist(address, self._client)
675
+
676
+ def submit_transaction(self, signed_tx_blob: str, fail_hard: bool = False) -> dict:
677
+ """
678
+ Submit a signed transaction to the network.
679
+
680
+ Args:
681
+ signed_tx_blob: Hex-encoded signed transaction from sign_xrpl_transaction()
682
+ fail_hard: If True, don't retry or relay to other servers on failure
683
+
684
+ Returns:
685
+ Dict with submission result including 'engine_result' and 'tx_json'
686
+ """
687
+ from xrpl.models.requests import SubmitOnly
688
+
689
+ request = SubmitOnly(tx_blob=signed_tx_blob, fail_hard=fail_hard)
690
+ response = self._client.request(request)
691
+ return response.result
692
+
693
+ def submit_and_wait(self, signed_tx_blob: str,
694
+ wallet_address: Optional[str] = None) -> dict:
695
+ """
696
+ Submit a signed transaction and wait for validation.
697
+
698
+ Args:
699
+ signed_tx_blob: Hex-encoded signed transaction
700
+ wallet_address: Optional address to include in the last_ledger_sequence
701
+
702
+ Returns:
703
+ Dict with validated transaction result
704
+ """
705
+ from xrpl.transaction import submit
706
+
707
+ response = submit(signed_tx_blob, self._client)
708
+ return response.result
709
+
710
+ def autofill_transaction(self, transaction) -> dict:
711
+ """
712
+ Autofill transaction fields (fee, sequence, last_ledger_sequence).
713
+
714
+ Takes a transaction and fills in network-specific fields automatically.
715
+
716
+ Args:
717
+ transaction: xrpl.models.Transaction or dict
718
+
719
+ Returns:
720
+ Dict with autofilled transaction fields
721
+ """
722
+ from xrpl.transaction import autofill
723
+
724
+ if hasattr(transaction, 'to_dict'):
725
+ # It's an xrpl model
726
+ autofilled = autofill(transaction, self._client)
727
+ return autofilled.to_dict()
728
+ else:
729
+ # It's a dict, need to convert to model first
730
+ from xrpl.models import Transaction
731
+ from xrpl.transaction import transaction_json_to_binary_codec_form
732
+
733
+ # For dict input, manually handle autofill
734
+ tx_dict = dict(transaction)
735
+
736
+ # Get sequence if not set
737
+ if 'sequence' not in tx_dict or tx_dict.get('sequence') is None:
738
+ account = tx_dict.get('account') or tx_dict.get('Account')
739
+ tx_dict['sequence'] = self.get_account_sequence(account)
740
+
741
+ return tx_dict
742
+
743
+ def get_transaction(self, tx_hash: str) -> dict:
744
+ """
745
+ Look up a transaction by its hash.
746
+
747
+ Args:
748
+ tx_hash: Transaction hash (hex string)
749
+
750
+ Returns:
751
+ Transaction details dict
752
+ """
753
+ from xrpl.models.requests import Tx
754
+
755
+ request = Tx(transaction=tx_hash)
756
+ response = self._client.request(request)
757
+ return response.result
758
+
759
+ @staticmethod
760
+ def fund_from_faucet(address: str, timeout: int = 60) -> dict:
761
+ """
762
+ Fund an account from the XRPL testnet faucet.
763
+
764
+ This only works on testnet/devnet.
765
+
766
+ Args:
767
+ address: The address to fund
768
+ timeout: Timeout in seconds (default: 60)
769
+
770
+ Returns:
771
+ Dict with faucet response including 'balance' and 'address'
772
+
773
+ Raises:
774
+ RuntimeError: If faucet request fails
775
+ """
776
+ import httpx
777
+ import time
778
+
779
+ faucet_url = "https://faucet.altnet.rippletest.net/accounts"
780
+
781
+ try:
782
+ response = httpx.post(
783
+ faucet_url,
784
+ json={"destination": address},
785
+ timeout=timeout
786
+ )
787
+ response.raise_for_status()
788
+ result = response.json()
789
+
790
+ # Wait a moment for the transaction to be validated
791
+ time.sleep(2)
792
+
793
+ return {
794
+ 'address': address,
795
+ 'balance': result.get('account', {}).get('balance'),
796
+ 'faucet_response': result
797
+ }
798
+ except Exception as e:
799
+ raise RuntimeError(f"Faucet request failed: {e}")
800
+
801
+
802
+
803
+ # =============================================================================
804
+ # Memo Helper Functions
805
+ # =============================================================================
806
+
807
+ def encode_memo_data(text: str) -> str:
808
+ """
809
+ Encode a text string as hex for XRPL memo data.
810
+
811
+ XRPL memos require hex-encoded data.
812
+
813
+ Args:
814
+ text: Plain text string to encode
815
+
816
+ Returns:
817
+ Uppercase hex-encoded string
818
+
819
+ Example:
820
+ >>> encode_memo_data("Hello")
821
+ '48656C6C6F'
822
+ """
823
+ return text.encode('utf-8').hex().upper()
824
+
825
+
826
+ def decode_memo_data(hex_data: str) -> str:
827
+ """
828
+ Decode hex-encoded XRPL memo data to text.
829
+
830
+ Args:
831
+ hex_data: Hex-encoded memo data
832
+
833
+ Returns:
834
+ Decoded text string
835
+
836
+ Example:
837
+ >>> decode_memo_data('48656C6C6F')
838
+ 'Hello'
839
+ """
840
+ return bytes.fromhex(hex_data).decode('utf-8')
841
+
842
+
843
+ def create_memo(data: str, memo_type: Optional[str] = None,
844
+ memo_format: Optional[str] = None) -> dict:
845
+ """
846
+ Create an XRPL memo dict with properly encoded fields.
847
+
848
+ This helper encodes plain text to hex format as required by XRPL.
849
+
850
+ Args:
851
+ data: The memo data (plain text, will be hex-encoded)
852
+ memo_type: Optional memo type (e.g., 'text/plain', will be hex-encoded)
853
+ memo_format: Optional memo format (e.g., 'text/plain', will be hex-encoded)
854
+
855
+ Returns:
856
+ Dict suitable for use in xrpl.models.Memo
857
+
858
+ Example:
859
+ >>> memo = create_memo("Hello World", memo_type="text/plain")
860
+ >>> memo['memo_data']
861
+ '48656C6C6F20576F726C64'
862
+ """
863
+ memo = {
864
+ 'memo_data': encode_memo_data(data)
865
+ }
866
+
867
+ if memo_type:
868
+ memo['memo_type'] = encode_memo_data(memo_type)
869
+
870
+ if memo_format:
871
+ memo['memo_format'] = encode_memo_data(memo_format)
872
+
873
+ return memo
874
+
875
+
876
+ def create_payment_with_memo(
877
+ account: str,
878
+ destination: str,
879
+ amount: str,
880
+ memo_text: str,
881
+ sequence: Optional[int] = None,
882
+ fee: str = "12",
883
+ memo_type: str = "text/plain"
884
+ ):
885
+ """
886
+ Create an XRPL Payment transaction with a memo.
887
+
888
+ This is a convenience function that handles memo encoding.
889
+
890
+ Args:
891
+ account: Source account address
892
+ destination: Destination account address
893
+ amount: Amount in drops (1 XRP = 1,000,000 drops)
894
+ memo_text: Plain text memo message
895
+ sequence: Account sequence number (required)
896
+ fee: Transaction fee in drops (default: "12")
897
+ memo_type: Memo type (default: "text/plain")
898
+
899
+ Returns:
900
+ xrpl.models.Payment transaction object
901
+
902
+ Example:
903
+ >>> tx = create_payment_with_memo(
904
+ ... account='rSourceAddress...',
905
+ ... destination='rDestAddress...',
906
+ ... amount='10000000', # 10 XRP
907
+ ... memo_text='Hello from threshold ECDSA!',
908
+ ... sequence=1
909
+ ... )
910
+ """
911
+ try:
912
+ from xrpl.models import Payment, Memo
913
+ except ImportError:
914
+ raise ImportError(
915
+ "xrpl-py is required. Install with: pip install xrpl-py"
916
+ )
917
+
918
+ memo_dict = create_memo(memo_text, memo_type=memo_type)
919
+
920
+ return Payment(
921
+ account=account,
922
+ destination=destination,
923
+ amount=amount,
924
+ sequence=sequence,
925
+ fee=fee,
926
+ memos=[Memo(**memo_dict)]
927
+ )
928
+
929
+
930
+ def get_transaction_memos(tx_result: dict) -> list:
931
+ """
932
+ Extract and decode memos from a transaction result.
933
+
934
+ Args:
935
+ tx_result: Transaction result dict from XRPL
936
+
937
+ Returns:
938
+ List of decoded memo dicts with 'data', 'type', 'format' keys
939
+ """
940
+ memos = []
941
+ tx_memos = tx_result.get('Memos', [])
942
+
943
+ for memo_wrapper in tx_memos:
944
+ memo = memo_wrapper.get('Memo', {})
945
+ decoded = {}
946
+
947
+ if 'MemoData' in memo:
948
+ try:
949
+ decoded['data'] = decode_memo_data(memo['MemoData'])
950
+ except Exception:
951
+ decoded['data'] = memo['MemoData'] # Keep hex if decode fails
952
+
953
+ if 'MemoType' in memo:
954
+ try:
955
+ decoded['type'] = decode_memo_data(memo['MemoType'])
956
+ except Exception:
957
+ decoded['type'] = memo['MemoType']
958
+
959
+ if 'MemoFormat' in memo:
960
+ try:
961
+ decoded['format'] = decode_memo_data(memo['MemoFormat'])
962
+ except Exception:
963
+ decoded['format'] = memo['MemoFormat']
964
+
965
+ memos.append(decoded)
966
+
967
+ return memos