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.
- charm/__init__.py +5 -0
- charm/adapters/__init__.py +0 -0
- charm/adapters/abenc_adapt_hybrid.py +90 -0
- charm/adapters/dabenc_adapt_hybrid.py +145 -0
- charm/adapters/ibenc_adapt_hybrid.py +72 -0
- charm/adapters/ibenc_adapt_identityhash.py +80 -0
- charm/adapters/kpabenc_adapt_hybrid.py +91 -0
- charm/adapters/pkenc_adapt_bchk05.py +121 -0
- charm/adapters/pkenc_adapt_chk04.py +91 -0
- charm/adapters/pkenc_adapt_hybrid.py +98 -0
- charm/adapters/pksig_adapt_naor01.py +89 -0
- charm/config.py +7 -0
- charm/core/__init__.py +0 -0
- charm/core/benchmark/benchmark_util.c +353 -0
- charm/core/benchmark/benchmark_util.h +61 -0
- charm/core/benchmark/benchmarkmodule.c +476 -0
- charm/core/benchmark/benchmarkmodule.h +162 -0
- charm/core/benchmark.cpython-313-darwin.so +0 -0
- charm/core/crypto/AES/AES.c +1464 -0
- charm/core/crypto/AES.cpython-313-darwin.so +0 -0
- charm/core/crypto/DES/DES.c +113 -0
- charm/core/crypto/DES.cpython-313-darwin.so +0 -0
- charm/core/crypto/DES3/DES3.c +26 -0
- charm/core/crypto/DES3.cpython-313-darwin.so +0 -0
- charm/core/crypto/__init__.py +0 -0
- charm/core/crypto/cryptobase/XOR.c +80 -0
- charm/core/crypto/cryptobase/_counter.c +496 -0
- charm/core/crypto/cryptobase/_counter.h +54 -0
- charm/core/crypto/cryptobase/block_template.c +900 -0
- charm/core/crypto/cryptobase/block_template.h +69 -0
- charm/core/crypto/cryptobase/cryptobasemodule.c +220 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt.h +90 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_argchk.h +44 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_cfg.h +186 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_cipher.h +941 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_custom.h +556 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_des.c +1912 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_hash.h +407 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_mac.h +496 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_macros.h +435 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_math.h +534 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_misc.h +103 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_pk.h +653 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_pkcs.h +90 -0
- charm/core/crypto/cryptobase/libtom/tomcrypt_prng.h +199 -0
- charm/core/crypto/cryptobase/stream_template.c +271 -0
- charm/core/crypto/cryptobase/strxor.c +229 -0
- charm/core/crypto/cryptobase.cpython-313-darwin.so +0 -0
- charm/core/engine/__init__.py +5 -0
- charm/core/engine/protocol.py +293 -0
- charm/core/engine/util.py +174 -0
- charm/core/math/__init__.py +0 -0
- charm/core/math/elliptic_curve/ecmodule.c +1986 -0
- charm/core/math/elliptic_curve/ecmodule.h +230 -0
- charm/core/math/elliptic_curve.cpython-313-darwin.so +0 -0
- charm/core/math/elliptic_curve.pyi +63 -0
- charm/core/math/integer/integermodule.c +2539 -0
- charm/core/math/integer/integermodule.h +145 -0
- charm/core/math/integer.cpython-313-darwin.so +0 -0
- charm/core/math/integer.pyi +76 -0
- charm/core/math/pairing/miracl/miracl_config.h +37 -0
- charm/core/math/pairing/miracl/miracl_interface.h +118 -0
- charm/core/math/pairing/miracl/miracl_interface2.h +126 -0
- charm/core/math/pairing/miracl/pairingmodule2.c +2094 -0
- charm/core/math/pairing/miracl/pairingmodule2.h +307 -0
- charm/core/math/pairing/pairingmodule.c +2230 -0
- charm/core/math/pairing/pairingmodule.h +241 -0
- charm/core/math/pairing/relic/pairingmodule3.c +1853 -0
- charm/core/math/pairing/relic/pairingmodule3.h +233 -0
- charm/core/math/pairing/relic/relic_interface.c +1337 -0
- charm/core/math/pairing/relic/relic_interface.h +217 -0
- charm/core/math/pairing/relic/test_relic.c +171 -0
- charm/core/math/pairing.cpython-313-darwin.so +0 -0
- charm/core/math/pairing.pyi +69 -0
- charm/core/utilities/base64.c +248 -0
- charm/core/utilities/base64.h +15 -0
- charm/schemes/__init__.py +0 -0
- charm/schemes/abenc/__init__.py +0 -0
- charm/schemes/abenc/abenc_accountability_jyjxgd20.py +647 -0
- charm/schemes/abenc/abenc_bsw07.py +146 -0
- charm/schemes/abenc/abenc_ca_cpabe_ar17.py +684 -0
- charm/schemes/abenc/abenc_dacmacs_yj14.py +298 -0
- charm/schemes/abenc/abenc_lsw08.py +159 -0
- charm/schemes/abenc/abenc_maabe_rw15.py +236 -0
- charm/schemes/abenc/abenc_maabe_yj14.py +297 -0
- charm/schemes/abenc/abenc_tbpre_lww14.py +309 -0
- charm/schemes/abenc/abenc_unmcpabe_yahk14.py +223 -0
- charm/schemes/abenc/abenc_waters09.py +144 -0
- charm/schemes/abenc/abenc_yct14.py +208 -0
- charm/schemes/abenc/abenc_yllc15.py +178 -0
- charm/schemes/abenc/ac17.py +248 -0
- charm/schemes/abenc/bsw07.py +141 -0
- charm/schemes/abenc/cgw15.py +277 -0
- charm/schemes/abenc/dabe_aw11.py +204 -0
- charm/schemes/abenc/dfa_fe12.py +144 -0
- charm/schemes/abenc/pk_hve08.py +179 -0
- charm/schemes/abenc/waters11.py +143 -0
- charm/schemes/aggrsign_MuSig.py +150 -0
- charm/schemes/aggrsign_bls.py +267 -0
- charm/schemes/blindsig_ps16.py +654 -0
- charm/schemes/chamhash_adm05.py +113 -0
- charm/schemes/chamhash_rsa_hw09.py +100 -0
- charm/schemes/commit/__init__.py +0 -0
- charm/schemes/commit/commit_gs08.py +77 -0
- charm/schemes/commit/commit_pedersen92.py +53 -0
- charm/schemes/encap_bchk05.py +62 -0
- charm/schemes/grpsig/__init__.py +0 -0
- charm/schemes/grpsig/groupsig_bgls04.py +114 -0
- charm/schemes/grpsig/groupsig_bgls04_var.py +115 -0
- charm/schemes/hibenc/__init__.py +0 -0
- charm/schemes/hibenc/hibenc_bb04.py +105 -0
- charm/schemes/hibenc/hibenc_lew11.py +193 -0
- charm/schemes/ibenc/__init__.py +0 -0
- charm/schemes/ibenc/clpkc_rp03.py +119 -0
- charm/schemes/ibenc/ibenc_CW13_z.py +168 -0
- charm/schemes/ibenc/ibenc_bb03.py +94 -0
- charm/schemes/ibenc/ibenc_bf01.py +121 -0
- charm/schemes/ibenc/ibenc_ckrs09.py +120 -0
- charm/schemes/ibenc/ibenc_cllww12_z.py +172 -0
- charm/schemes/ibenc/ibenc_lsw08.py +120 -0
- charm/schemes/ibenc/ibenc_sw05.py +238 -0
- charm/schemes/ibenc/ibenc_waters05.py +144 -0
- charm/schemes/ibenc/ibenc_waters05_z.py +164 -0
- charm/schemes/ibenc/ibenc_waters09.py +107 -0
- charm/schemes/ibenc/ibenc_waters09_z.py +147 -0
- charm/schemes/joye_scheme.py +106 -0
- charm/schemes/lem_scheme.py +207 -0
- charm/schemes/pk_fre_ccv11.py +107 -0
- charm/schemes/pk_vrf.py +127 -0
- charm/schemes/pkenc/__init__.py +0 -0
- charm/schemes/pkenc/pkenc_cs98.py +108 -0
- charm/schemes/pkenc/pkenc_elgamal85.py +122 -0
- charm/schemes/pkenc/pkenc_gm82.py +98 -0
- charm/schemes/pkenc/pkenc_paillier99.py +118 -0
- charm/schemes/pkenc/pkenc_rabin.py +254 -0
- charm/schemes/pkenc/pkenc_rsa.py +186 -0
- charm/schemes/pksig/__init__.py +0 -0
- charm/schemes/pksig/pksig_CW13_z.py +135 -0
- charm/schemes/pksig/pksig_bls04.py +87 -0
- charm/schemes/pksig/pksig_boyen.py +156 -0
- charm/schemes/pksig/pksig_chch.py +97 -0
- charm/schemes/pksig/pksig_chp.py +70 -0
- charm/schemes/pksig/pksig_cl03.py +150 -0
- charm/schemes/pksig/pksig_cl04.py +87 -0
- charm/schemes/pksig/pksig_cllww12_z.py +142 -0
- charm/schemes/pksig/pksig_cyh.py +132 -0
- charm/schemes/pksig/pksig_dsa.py +76 -0
- charm/schemes/pksig/pksig_ecdsa.py +71 -0
- charm/schemes/pksig/pksig_hess.py +104 -0
- charm/schemes/pksig/pksig_hw.py +110 -0
- charm/schemes/pksig/pksig_lamport.py +63 -0
- charm/schemes/pksig/pksig_ps01.py +135 -0
- charm/schemes/pksig/pksig_ps02.py +124 -0
- charm/schemes/pksig/pksig_ps03.py +119 -0
- charm/schemes/pksig/pksig_rsa_hw09.py +206 -0
- charm/schemes/pksig/pksig_schnorr91.py +77 -0
- charm/schemes/pksig/pksig_waters.py +115 -0
- charm/schemes/pksig/pksig_waters05.py +121 -0
- charm/schemes/pksig/pksig_waters09.py +121 -0
- charm/schemes/pre_mg07.py +150 -0
- charm/schemes/prenc/pre_afgh06.py +126 -0
- charm/schemes/prenc/pre_bbs98.py +123 -0
- charm/schemes/prenc/pre_nal16.py +216 -0
- charm/schemes/protocol_a01.py +272 -0
- charm/schemes/protocol_ao00.py +215 -0
- charm/schemes/protocol_cns07.py +274 -0
- charm/schemes/protocol_schnorr91.py +125 -0
- charm/schemes/sigma1.py +64 -0
- charm/schemes/sigma2.py +129 -0
- charm/schemes/sigma3.py +126 -0
- charm/schemes/threshold/__init__.py +59 -0
- charm/schemes/threshold/dkls23_dkg.py +556 -0
- charm/schemes/threshold/dkls23_presign.py +1089 -0
- charm/schemes/threshold/dkls23_sign.py +761 -0
- charm/schemes/threshold/xrpl_wallet.py +967 -0
- charm/test/__init__.py +0 -0
- charm/test/adapters/__init__.py +0 -0
- charm/test/adapters/abenc_adapt_hybrid_test.py +29 -0
- charm/test/adapters/dabenc_adapt_hybrid_test.py +56 -0
- charm/test/adapters/ibenc_adapt_hybrid_test.py +36 -0
- charm/test/adapters/ibenc_adapt_identityhash_test.py +32 -0
- charm/test/adapters/kpabenc_adapt_hybrid_test.py +30 -0
- charm/test/benchmark/abenc_yllc15_bench.py +92 -0
- charm/test/benchmark/benchmark_test.py +148 -0
- charm/test/benchmark_threshold.py +260 -0
- charm/test/conftest.py +38 -0
- charm/test/fuzz/__init__.py +1 -0
- charm/test/fuzz/conftest.py +5 -0
- charm/test/fuzz/fuzz_policy_parser.py +76 -0
- charm/test/fuzz/fuzz_serialization.py +83 -0
- charm/test/schemes/__init__.py +0 -0
- charm/test/schemes/abenc/__init__.py +0 -0
- charm/test/schemes/abenc/abenc_bsw07_test.py +39 -0
- charm/test/schemes/abenc/abenc_dacmacs_yj14_test.py +16 -0
- charm/test/schemes/abenc/abenc_lsw08_test.py +33 -0
- charm/test/schemes/abenc/abenc_maabe_yj14_test.py +16 -0
- charm/test/schemes/abenc/abenc_tbpre_lww14_test.py +16 -0
- charm/test/schemes/abenc/abenc_waters09_test.py +38 -0
- charm/test/schemes/abenc/abenc_yllc15_test.py +74 -0
- charm/test/schemes/chamhash_adm05_test.py +31 -0
- charm/test/schemes/chamhash_rsa_hw09_test.py +29 -0
- charm/test/schemes/commit/__init__.py +0 -0
- charm/test/schemes/commit/commit_gs08_test.py +24 -0
- charm/test/schemes/commit/commit_pedersen92_test.py +26 -0
- charm/test/schemes/dabe_aw11_test.py +45 -0
- charm/test/schemes/encap_bchk05_test.py +21 -0
- charm/test/schemes/grpsig/__init__.py +0 -0
- charm/test/schemes/grpsig/groupsig_bgls04_test.py +35 -0
- charm/test/schemes/grpsig/groupsig_bgls04_var_test.py +39 -0
- charm/test/schemes/hibenc/__init__.py +0 -0
- charm/test/schemes/hibenc/hibenc_bb04_test.py +28 -0
- charm/test/schemes/ibenc/__init__.py +0 -0
- charm/test/schemes/ibenc/ibenc_bb03_test.py +26 -0
- charm/test/schemes/ibenc/ibenc_bf01_test.py +24 -0
- charm/test/schemes/ibenc/ibenc_ckrs09_test.py +25 -0
- charm/test/schemes/ibenc/ibenc_lsw08_test.py +31 -0
- charm/test/schemes/ibenc/ibenc_sw05_test.py +32 -0
- charm/test/schemes/ibenc/ibenc_waters05_test.py +31 -0
- charm/test/schemes/ibenc/ibenc_waters09_test.py +27 -0
- charm/test/schemes/pk_vrf_test.py +29 -0
- charm/test/schemes/pkenc/__init__.py +0 -0
- charm/test/schemes/pkenc_test.py +255 -0
- charm/test/schemes/pksig/__init__.py +0 -0
- charm/test/schemes/pksig_test.py +376 -0
- charm/test/schemes/rsa_alg_test.py +340 -0
- charm/test/schemes/threshold_test.py +1792 -0
- charm/test/serialize/__init__.py +0 -0
- charm/test/serialize/serialize_test.py +40 -0
- charm/test/toolbox/__init__.py +0 -0
- charm/test/toolbox/conversion_test.py +30 -0
- charm/test/toolbox/ecgroup_test.py +53 -0
- charm/test/toolbox/integer_arithmetic_test.py +441 -0
- charm/test/toolbox/paddingschemes_test.py +238 -0
- charm/test/toolbox/policy_parser_stress_test.py +969 -0
- charm/test/toolbox/secretshare_test.py +28 -0
- charm/test/toolbox/symcrypto_test.py +108 -0
- charm/test/toolbox/test_policy_expression.py +16 -0
- charm/test/vectors/__init__.py +1 -0
- charm/test/vectors/test_bls_vectors.py +289 -0
- charm/test/vectors/test_pedersen_vectors.py +315 -0
- charm/test/vectors/test_schnorr_vectors.py +368 -0
- charm/test/zkp_compiler/__init__.py +9 -0
- charm/test/zkp_compiler/benchmark_zkp.py +258 -0
- charm/test/zkp_compiler/test_and_proof.py +240 -0
- charm/test/zkp_compiler/test_batch_verify.py +248 -0
- charm/test/zkp_compiler/test_dleq_proof.py +264 -0
- charm/test/zkp_compiler/test_or_proof.py +231 -0
- charm/test/zkp_compiler/test_proof_serialization.py +121 -0
- charm/test/zkp_compiler/test_range_proof.py +241 -0
- charm/test/zkp_compiler/test_representation_proof.py +325 -0
- charm/test/zkp_compiler/test_schnorr_proof.py +221 -0
- charm/test/zkp_compiler/test_thread_safety.py +169 -0
- charm/test/zkp_compiler/test_zkp_parser.py +139 -0
- charm/toolbox/ABEnc.py +26 -0
- charm/toolbox/ABEncMultiAuth.py +66 -0
- charm/toolbox/ABEnumeric.py +800 -0
- charm/toolbox/Commit.py +24 -0
- charm/toolbox/DFA.py +89 -0
- charm/toolbox/FSA.py +1254 -0
- charm/toolbox/Hash.py +39 -0
- charm/toolbox/IBEnc.py +62 -0
- charm/toolbox/IBSig.py +64 -0
- charm/toolbox/PKEnc.py +66 -0
- charm/toolbox/PKSig.py +56 -0
- charm/toolbox/PREnc.py +32 -0
- charm/toolbox/ZKProof.py +289 -0
- charm/toolbox/__init__.py +0 -0
- charm/toolbox/bitstring.py +49 -0
- charm/toolbox/broadcast.py +220 -0
- charm/toolbox/conversion.py +100 -0
- charm/toolbox/eccurve.py +149 -0
- charm/toolbox/ecgroup.py +143 -0
- charm/toolbox/enum.py +60 -0
- charm/toolbox/hash_module.py +91 -0
- charm/toolbox/integergroup.py +323 -0
- charm/toolbox/iterate.py +22 -0
- charm/toolbox/matrixops.py +76 -0
- charm/toolbox/mpc_utils.py +296 -0
- charm/toolbox/msp.py +175 -0
- charm/toolbox/mta.py +985 -0
- charm/toolbox/node.py +120 -0
- charm/toolbox/ot/__init__.py +22 -0
- charm/toolbox/ot/base_ot.py +374 -0
- charm/toolbox/ot/dpf.py +642 -0
- charm/toolbox/ot/mpfss.py +228 -0
- charm/toolbox/ot/ot_extension.py +589 -0
- charm/toolbox/ot/silent_ot.py +378 -0
- charm/toolbox/paddingschemes.py +423 -0
- charm/toolbox/paddingschemes_test.py +238 -0
- charm/toolbox/pairingcurves.py +85 -0
- charm/toolbox/pairinggroup.py +186 -0
- charm/toolbox/policy_expression_spec.py +70 -0
- charm/toolbox/policytree.py +189 -0
- charm/toolbox/reCompiler.py +346 -0
- charm/toolbox/redundancyschemes.py +65 -0
- charm/toolbox/schemebase.py +188 -0
- charm/toolbox/secretshare.py +104 -0
- charm/toolbox/secretutil.py +174 -0
- charm/toolbox/securerandom.py +73 -0
- charm/toolbox/sigmaprotocol.py +46 -0
- charm/toolbox/specialprimes.py +45 -0
- charm/toolbox/symcrypto.py +279 -0
- charm/toolbox/threshold_sharing.py +553 -0
- charm/toolbox/xmlserialize.py +94 -0
- charm/toolbox/zknode.py +105 -0
- charm/zkp_compiler/__init__.py +89 -0
- charm/zkp_compiler/and_proof.py +460 -0
- charm/zkp_compiler/batch_verify.py +324 -0
- charm/zkp_compiler/dleq_proof.py +423 -0
- charm/zkp_compiler/or_proof.py +305 -0
- charm/zkp_compiler/range_proof.py +417 -0
- charm/zkp_compiler/representation_proof.py +466 -0
- charm/zkp_compiler/schnorr_proof.py +273 -0
- charm/zkp_compiler/thread_safe.py +150 -0
- charm/zkp_compiler/zk_demo.py +489 -0
- charm/zkp_compiler/zkp_factory.py +330 -0
- charm/zkp_compiler/zkp_generator.py +370 -0
- charm/zkp_compiler/zkparser.py +269 -0
- charm_crypto_framework-0.61.1.dist-info/METADATA +337 -0
- charm_crypto_framework-0.61.1.dist-info/RECORD +323 -0
- charm_crypto_framework-0.61.1.dist-info/WHEEL +5 -0
- charm_crypto_framework-0.61.1.dist-info/licenses/LICENSE.txt +165 -0
- 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
|