diff --git a/chainlibpy/amino/transaction.py b/chainlibpy/amino/transaction.py new file mode 100644 index 0000000..85c7f0d --- /dev/null +++ b/chainlibpy/amino/transaction.py @@ -0,0 +1,96 @@ +import base64 +import hashlib +import json +from typing import List + +import ecdsa + +from chainlibpy.amino import StdFee, StdSignDoc, SyncMode +from chainlibpy.amino.message import Msg +from chainlibpy.amino.tx import Pubkey, Signature, StdTx +from chainlibpy.wallet import Wallet + + +class Transaction: + """A Cosmos transaction. + + After initialization, one or more token transfers can be added by + calling the `add_transfer()` method. Finally, call `get_pushable()` + to get a signed transaction that can be pushed to the `POST /txs` + endpoint of the Cosmos REST API. + """ + + def __init__( + self, + wallet: Wallet, + account_num: int, + sequence: int, + fee: StdFee = StdFee.default(), + memo: str = "", + chain_id: str = "crypto-org-chain-mainnet-1", + sync_mode: SyncMode = "sync", + timeout_height: int = 0, + multi_sign_address: str = None, + ) -> None: + self._multi_sign_address = multi_sign_address + self._wallet = wallet + self._account_num = str(account_num) + self._sequence = str(sequence) + self._fee = fee + self._memo = memo + self._chain_id = chain_id + self._sync_mode = sync_mode + self._msgs: List[Msg] = [] + self._timeout_height = str(timeout_height) + + def add_msg(self, msg: Msg): + self._msgs.append(msg) + + def get_pushable(self) -> dict: + """get the request post to the /txs.""" + std_tx = StdTx(self._msgs, self._fee, self._memo, self._timeout_height, [self.signature]) + + pushable_tx = { + "tx": std_tx.to_dict(), + "mode": self._sync_mode, + } + return pushable_tx + + @property + def signature(self) -> Signature: + pubkey = self._wallet.public_key + base64_pubkey = base64.b64encode(pubkey).decode("utf-8") + pubkey = Pubkey(value=base64_pubkey) + raw_signature = self.sign() + sig_str = base64.b64encode(raw_signature).decode("utf-8") + signature = Signature(sig_str, pubkey, self._account_num, self._sequence) + return signature + + def sign(self) -> bytes: + sign_doc = self._get_sign_doc() + message_str = json.dumps( + sign_doc.to_dict(), separators=(",", ":"), sort_keys=True + ) + message_bytes = message_str.encode("utf-8") + + privkey = ecdsa.SigningKey.from_string( + self._wallet.private_key, curve=ecdsa.SECP256k1 + ) + signature_compact = privkey.sign_deterministic( + message_bytes, + hashfunc=hashlib.sha256, + sigencode=ecdsa.util.sigencode_string_canonize, + ) + return signature_compact + + def _get_sign_doc(self) -> StdSignDoc: + sign_doc = StdSignDoc( + account_number=self._account_num, + sequence=self._sequence, + chain_id=self._chain_id, + memo=self._memo, + fee=self._fee.to_dict(), + msgs=self._msgs, + timeout_height=self._timeout_height, + ) + return sign_doc diff --git a/chainlibpy/generated/cosmos/crypto/multisig/keys_pb2.py b/chainlibpy/generated/cosmos/crypto/multisig/keys_pb2.py new file mode 100644 index 0000000..abb1720 --- /dev/null +++ b/chainlibpy/generated/cosmos/crypto/multisig/keys_pb2.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: cosmos/crypto/multisig/keys.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from gogoproto import gogo_pb2 as gogoproto_dot_gogo__pb2 +from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='cosmos/crypto/multisig/keys.proto', + package='cosmos.crypto.multisig', + syntax='proto3', + serialized_options=b'Z1github.com/cosmos/cosmos-sdk/crypto/keys/multisig', + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n!cosmos/crypto/multisig/keys.proto\x12\x16\x63osmos.crypto.multisig\x1a\x14gogoproto/gogo.proto\x1a\x19google/protobuf/any.proto\"\x8c\x01\n\x11LegacyAminoPubKey\x12\'\n\tthreshold\x18\x01 \x01(\rB\x14\xf2\xde\x1f\x10yaml:\"threshold\"\x12H\n\x0bpublic_keys\x18\x02 \x03(\x0b\x32\x14.google.protobuf.AnyB\x1d\xe2\xde\x1f\x07PubKeys\xf2\xde\x1f\x0eyaml:\"pubkeys\":\x04\x88\xa0\x1f\x00\x42\x33Z1github.com/cosmos/cosmos-sdk/crypto/keys/multisigb\x06proto3' + , + dependencies=[gogoproto_dot_gogo__pb2.DESCRIPTOR,google_dot_protobuf_dot_any__pb2.DESCRIPTOR,]) + + + + +_LEGACYAMINOPUBKEY = _descriptor.Descriptor( + name='LegacyAminoPubKey', + full_name='cosmos.crypto.multisig.LegacyAminoPubKey', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='threshold', full_name='cosmos.crypto.multisig.LegacyAminoPubKey.threshold', index=0, + number=1, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'\362\336\037\020yaml:\"threshold\"', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='public_keys', full_name='cosmos.crypto.multisig.LegacyAminoPubKey.public_keys', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'\342\336\037\007PubKeys\362\336\037\016yaml:\"pubkeys\"', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=b'\210\240\037\000', + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=111, + serialized_end=251, +) + +_LEGACYAMINOPUBKEY.fields_by_name['public_keys'].message_type = google_dot_protobuf_dot_any__pb2._ANY +DESCRIPTOR.message_types_by_name['LegacyAminoPubKey'] = _LEGACYAMINOPUBKEY +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +LegacyAminoPubKey = _reflection.GeneratedProtocolMessageType('LegacyAminoPubKey', (_message.Message,), { + 'DESCRIPTOR' : _LEGACYAMINOPUBKEY, + '__module__' : 'cosmos.crypto.multisig.keys_pb2' + # @@protoc_insertion_point(class_scope:cosmos.crypto.multisig.LegacyAminoPubKey) + }) +_sym_db.RegisterMessage(LegacyAminoPubKey) + + +DESCRIPTOR._options = None +_LEGACYAMINOPUBKEY.fields_by_name['threshold']._options = None +_LEGACYAMINOPUBKEY.fields_by_name['public_keys']._options = None +_LEGACYAMINOPUBKEY._options = None +# @@protoc_insertion_point(module_scope) diff --git a/chainlibpy/generated/cosmos/crypto/multisig/keys_pb2_grpc.py b/chainlibpy/generated/cosmos/crypto/multisig/keys_pb2_grpc.py new file mode 100644 index 0000000..2daafff --- /dev/null +++ b/chainlibpy/generated/cosmos/crypto/multisig/keys_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/chainlibpy/grpc_client.py b/chainlibpy/grpc_client.py index b10eb28..d9d4f55 100644 --- a/chainlibpy/grpc_client.py +++ b/chainlibpy/grpc_client.py @@ -59,6 +59,15 @@ class NetworkConfig: exponent=8, derivation_path="m/44'/1'/0'/0/0", ), + "devnet": NetworkConfig( + grpc_endpoint="0.0.0.0:26653", + chain_id="chain_id", + address_prefix="cro", + coin_denom="cro", + coin_base_denom="basecro", + derivation_path="m/44'/394'/0'/0/0", + exponent=8, + ), } @@ -160,4 +169,4 @@ def broadcast_transaction( else: raise TypeError("Unexcepted mode, should be [sync, async, block]") - self.tx_client.BroadcastTx(BroadcastTxRequest(tx_bytes=tx_byte, mode=_mode)) + return self.tx_client.BroadcastTx(BroadcastTxRequest(tx_bytes=tx_byte, mode=_mode)) diff --git a/chainlibpy/multisign/__init__.py b/chainlibpy/multisign/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chainlibpy/multisign/bitarray.py b/chainlibpy/multisign/bitarray.py new file mode 100644 index 0000000..d6f58a3 --- /dev/null +++ b/chainlibpy/multisign/bitarray.py @@ -0,0 +1,74 @@ +import base64 + +from chainlibpy.amino.basic import BasicObj +from chainlibpy.generated.cosmos.crypto.multisig.v1beta1.multisig_pb2 import ( + CompactBitArray as ProtoCompactBitArray, +) +from chainlibpy.multisign.bits import Bits + + +class CompactBitArray(BasicObj): + MaxInt32 = 2147483647 + + def __init__(self, bits: int): + self.extra_bits_stored = 0 + self.elems = bytearray() + if bits <= 0: + raise Exception(f"invalid bits {bits}") + n_elems = (bits + 7) // 8 + if n_elems <= 0 or n_elems > self.MaxInt32: + raise Exception(f"invalid bits {bits}") + self.extra_bits_stored = bits % 8 + self.elems = bytearray([0] * n_elems) + + def __repr__(self): + elems = base64.b64encode(self.elems).decode('utf-8') + return f"extra_bits_stored:{self.extra_bits_stored}, elems:{elems}" + + def bit_array(self) -> ProtoCompactBitArray: + return ProtoCompactBitArray( + extra_bits_stored=self.extra_bits_stored, + elems=bytes(self.elems) + ) + + def count(self) -> int: + """returns the number of bits in the bitarray.""" + if self.extra_bits_stored == 0: + return len(self.elems) * 8 + return (len(self.elems) - 1) * 8 + int(self.extra_bits_stored) + + def get_index(self, index: int) -> bool: + """returns the bit at index i within the bit array. + + The behavior is undefined if i >= self.count() + """ + if index < 0 or index >= self.count(): + return False + return (self.elems[index >> 3] & (1 << (7 - (index % 8)))) > 0 + + def set_index(self, i: int, v: bool) -> bool: + """set_index sets the bit at index i within the bit array. + + Returns true if and only if the operation succeeded. The + behavior is undefined if i >= self.count() + """ + if i < 0 or i >= self.count(): + return False + + if v: + self.elems[i >> 3] |= (1 << (7 - (i % 8))) + else: + self.elems[i >> 3] &= ~(1 << (7 - (i % 8))) + return True + + def num_true_bits_before(self, index: int) -> int: + ones_count = 0 + max_ = self.count() + index = min(index, max_) + elem = 0 + while True: + if elem*8+7 >= index: + ones_count += Bits.ones_count8(self.elems[elem]) >> (7 - (index % 8) + 1) + return ones_count + ones_count += Bits.ones_count8(self.elems[elem]) + elem += 1 diff --git a/chainlibpy/multisign/bits.py b/chainlibpy/multisign/bits.py new file mode 100644 index 0000000..ef58d44 --- /dev/null +++ b/chainlibpy/multisign/bits.py @@ -0,0 +1,27 @@ +class Bits(object): + pop8tab = [ + 0x00, 0x01, 0x01, 0x02, 0x01, 0x02, 0x02, 0x03, 0x01, 0x02, 0x02, 0x03, 0x02, 0x03, 0x03, 0x04, + 0x01, 0x02, 0x02, 0x03, 0x02, 0x03, 0x03, 0x04, 0x02, 0x03, 0x03, 0x04, 0x03, 0x04, 0x04, 0x05, + 0x01, 0x02, 0x02, 0x03, 0x02, 0x03, 0x03, 0x04, 0x02, 0x03, 0x03, 0x04, 0x03, 0x04, 0x04, 0x05, + 0x02, 0x03, 0x03, 0x04, 0x03, 0x04, 0x04, 0x05, 0x03, 0x04, 0x04, 0x05, 0x04, 0x05, 0x05, 0x06, + 0x01, 0x02, 0x02, 0x03, 0x02, 0x03, 0x03, 0x04, 0x02, 0x03, 0x03, 0x04, 0x03, 0x04, 0x04, 0x05, + 0x02, 0x03, 0x03, 0x04, 0x03, 0x04, 0x04, 0x05, 0x03, 0x04, 0x04, 0x05, 0x04, 0x05, 0x05, 0x06, + 0x02, 0x03, 0x03, 0x04, 0x03, 0x04, 0x04, 0x05, 0x03, 0x04, 0x04, 0x05, 0x04, 0x05, 0x05, 0x06, + 0x03, 0x04, 0x04, 0x05, 0x04, 0x05, 0x05, 0x06, 0x04, 0x05, 0x05, 0x06, 0x05, 0x06, 0x06, 0x07, + 0x01, 0x02, 0x02, 0x03, 0x02, 0x03, 0x03, 0x04, 0x02, 0x03, 0x03, 0x04, 0x03, 0x04, 0x04, 0x05, + 0x02, 0x03, 0x03, 0x04, 0x03, 0x04, 0x04, 0x05, 0x03, 0x04, 0x04, 0x05, 0x04, 0x05, 0x05, 0x06, + 0x02, 0x03, 0x03, 0x04, 0x03, 0x04, 0x04, 0x05, 0x03, 0x04, 0x04, 0x05, 0x04, 0x05, 0x05, 0x06, + 0x03, 0x04, 0x04, 0x05, 0x04, 0x05, 0x05, 0x06, 0x04, 0x05, 0x05, 0x06, 0x05, 0x06, 0x06, 0x07, + 0x02, 0x03, 0x03, 0x04, 0x03, 0x04, 0x04, 0x05, 0x03, 0x04, 0x04, 0x05, 0x04, 0x05, 0x05, 0x06, + 0x03, 0x04, 0x04, 0x05, 0x04, 0x05, 0x05, 0x06, 0x04, 0x05, 0x05, 0x06, 0x05, 0x06, 0x06, 0x07, + 0x03, 0x04, 0x04, 0x05, 0x04, 0x05, 0x05, 0x06, 0x04, 0x05, 0x05, 0x06, 0x05, 0x06, 0x06, 0x07, + 0x04, 0x05, 0x05, 0x06, 0x05, 0x06, 0x06, 0x07, 0x05, 0x06, 0x06, 0x07, 0x06, 0x07, 0x07, 0x08 + ] + + @classmethod + def ones_count8(cls, x: int) -> int: + """returns the number of one bits ("population count") in x. + + from: go/match/bits/bits.go + """ + return int(cls.pop8tab[x]) diff --git a/chainlibpy/multisign/signature.py b/chainlibpy/multisign/signature.py new file mode 100644 index 0000000..e2fef99 --- /dev/null +++ b/chainlibpy/multisign/signature.py @@ -0,0 +1,113 @@ +import base64 +from dataclasses import dataclass +from typing import List + +from chainlibpy.generated.cosmos.tx.v1beta1.tx_pb2 import SignerInfo, ModeInfo + +from chainlibpy.generated.cosmos.crypto.multisig.keys_pb2 import LegacyAminoPubKey +from google.protobuf.any_pb2 import Any as ProtoAny + +from chainlibpy.amino.basic import BasicObj +from chainlibpy.generated.cosmos.crypto.multisig.v1beta1.multisig_pb2 import ( + CompactBitArray as ProtoCompactBitArray, +) +from chainlibpy.generated.cosmos.crypto.secp256k1.keys_pb2 import PubKey as ProtoPubKey +from chainlibpy.generated.cosmos.tx.signing.v1beta1.signing_pb2 import SignMode +from chainlibpy.multisign.bitarray import CompactBitArray + + + +def packed_to_any(m): + packed = ProtoAny() + packed.Pack(m, type_url_prefix="/") + return packed + + +@dataclass(init=True, repr=True, eq=True, order=True, frozen=True) +class SingleSignatureData(BasicObj): + sign_mode: SignMode + signature: bytes + + +class SingleSignatureV2(object): + pub_key: ProtoPubKey + data: SingleSignatureData + sequence: int + + def __init__(self, pub_key: bytes, sequence: int, raw_sig: bytes): + self.pub_key = ProtoPubKey(key=pub_key) + self.data = SingleSignatureData( + sign_mode=SignMode.SIGN_MODE_LEGACY_AMINO_JSON, + signature=raw_sig + ) + self.sequence = sequence + + +class MultiSignatureData(object): + def __init__(self, n: int): + self._bit_array = CompactBitArray(n) + self.signatures = [] + + def __repr__(self): + signatures = [base64.b64encode(s).decode('utf-8') for s in self.signatures] + return f"signatures: {signatures}, bit_array: {self.bit_array}" + + @property + def bit_array(self) -> ProtoCompactBitArray: + return self._bit_array.bit_array() + + def add_signature_from_pubkey( + self, + sig: bytes, + pubkey: ProtoPubKey, + all_pubkeys: List[ProtoPubKey] + ): + index = all_pubkeys.index(packed_to_any(pubkey)) + new_sig_index = self._bit_array.num_true_bits_before(index) + + # replace the old + if self._bit_array.get_index(index): + self.signatures[new_sig_index] = sig + return + self._bit_array.set_index(index, True) + + # Optimization if the index is the greatest index + if new_sig_index == len(self.signatures): + self.signatures.append(sig) + return + + # insert at the new_sig_index + self.signatures.insert(new_sig_index, sig) + + def add_single_sig_v2( + self, + single_sig_v2: SingleSignatureV2, + all_pubkeys: List[ProtoPubKey] + ): + self.add_signature_from_pubkey( + single_sig_v2.data.signature, + single_sig_v2.pub_key, + all_pubkeys + ) + + +def gen_packed_send_msg( + sequence: int, + multi_pubkey: LegacyAminoPubKey, + bitarray: ProtoCompactBitArray +) -> SignerInfo: + multi_pubkey_packed = ProtoAny() + multi_pubkey_packed.Pack(multi_pubkey, type_url_prefix="/") + + # Prepare auth info + signal_mode_info = ModeInfo(single=ModeInfo.Single(mode=SignMode.SIGN_MODE_LEGACY_AMINO_JSON)) + mode_infos = [signal_mode_info] * multi_pubkey.threshold + multi = ModeInfo.Multi(bitarray=bitarray, mode_infos=mode_infos) + mode_info = ModeInfo(multi=multi) + signer_info = SignerInfo( + public_key=multi_pubkey_packed, + mode_info=mode_info, + sequence=sequence, + ) + return signer_info + diff --git a/chainlibpy/transaction.py b/chainlibpy/transaction.py index b10754f..eb66189 100644 --- a/chainlibpy/transaction.py +++ b/chainlibpy/transaction.py @@ -4,11 +4,18 @@ from typing import List, Optional +from chainlibpy.multisign.signature import MultiSignatureData, SingleSignatureV2 from google.protobuf import any_pb2, message +from google.protobuf.any_pb2 import Any as ProtoAny from chainlibpy.generated.cosmos.base.v1beta1.coin_pb2 import Coin +from chainlibpy.generated.cosmos.bank.v1beta1.tx_pb2 import MsgSend from chainlibpy.generated.cosmos.crypto.secp256k1.keys_pb2 import PubKey +from chainlibpy.generated.cosmos.crypto.multisig.keys_pb2 import LegacyAminoPubKey from chainlibpy.generated.cosmos.tx.signing.v1beta1.signing_pb2 import SignMode +from chainlibpy.generated.cosmos.crypto.multisig.v1beta1.multisig_pb2 import ( + CompactBitArray as ProtoCompactBitArray, MultiSignature, +) from chainlibpy.generated.cosmos.tx.v1beta1.tx_pb2 import ( AuthInfo, Fee, @@ -27,16 +34,16 @@ class Transaction: def __init__( - self, - chain_id: str, - from_wallets: List[Wallet], - msgs: List[message.Message], - account_number: int, - client: "GrpcClient", - gas_limit: int = DEFAULT_GAS_LIMIT, - fee: Optional[List[Coin]] = None, - memo: str = "", - timeout_height: Optional[int] = None, + self, + chain_id: str, + from_wallets: List[Wallet], + msgs: List[message.Message], + account_number: int, + client: "GrpcClient", + gas_limit: int = DEFAULT_GAS_LIMIT, + fee: Optional[List[Coin]] = None, + memo: str = "", + timeout_height: Optional[int] = None, ) -> None: """Transaction class to prepare unsigned transaction and generate signed transaction with signatures. @@ -142,3 +149,89 @@ def signed_tx(self) -> Tx: raise TypeError("Set signatures first before getting signed_tx") return Tx(body=self.tx_body, auth_info=self.auth_info, signatures=self._signatures) + + +def gen_muli_signer_info( + sequence: int, + multi_pubkey: LegacyAminoPubKey, + bitarray: ProtoCompactBitArray +) -> SignerInfo: + multi_pubkey_packed = ProtoAny().Pack(multi_pubkey, type_url_prefix="/") + + # Prepare auth info + signal_mode_info = ModeInfo(single=ModeInfo.Single(mode=SignMode.SIGN_MODE_LEGACY_AMINO_JSON)) + mode_infos = [signal_mode_info] * multi_pubkey.threshold + multi = ModeInfo.Multi(bitarray=bitarray, mode_infos=mode_infos) + mode_info = ModeInfo(multi=multi) + signer_info = SignerInfo( + public_key=multi_pubkey_packed, + mode_info=mode_info, + sequence=sequence, + ) + return signer_info + + +def gen_packed_send_msg(from_address: str, to_address: str, amount: List[Coin]) -> ProtoAny: + msg_send = MsgSend(from_address=from_address, to_address=to_address, amount=amount) + send_msg_packed = ProtoAny() + send_msg_packed.Pack(msg_send, type_url_prefix="/") + + return send_msg_packed + + +def gen_multi_signer_info( + sequence: int, + multi_pubkey: LegacyAminoPubKey, + bitarray: ProtoCompactBitArray +) -> SignerInfo: + import pdb; + pdb.set_trace() + multi_pubkey_packed = ProtoAny() + multi_pubkey_packed.Pack(multi_pubkey, type_url_prefix="/") + + # Prepare auth info + signal_mode_info = ModeInfo(single=ModeInfo.Single(mode=SignMode.SIGN_MODE_LEGACY_AMINO_JSON)) + mode_infos = [signal_mode_info] * multi_pubkey.threshold + multi = ModeInfo.Multi(bitarray=bitarray, mode_infos=mode_infos) + mode_info = ModeInfo(multi=multi) + signer_info = SignerInfo( + public_key=multi_pubkey_packed, + mode_info=mode_info, + sequence=sequence, + ) + return signer_info + + +def gen_multi_tx( + packed_msgs: List[ProtoAny], + multi_pubkey: LegacyAminoPubKey, + signature_batch: List[SingleSignatureV2], + sequence: int, + fee: Optional[List[Coin]] = None, + memo: str = "", + gas_limit: int = 2000, +) -> Tx: + if multi_pubkey.threshold > len(signature_batch): + raise Exception("single signatures should >= threshold") + + all_proto_pubkeys = [p for p in multi_pubkey.public_keys] + n = len(signature_batch) + multi_sign_data = MultiSignatureData(n) + for sig_v2 in signature_batch: + multi_sign_data.add_single_sig_v2(sig_v2, all_proto_pubkeys) + + signer_info = gen_muli_signer_info(sequence, multi_pubkey, multi_sign_data.bit_array) + signer_infos = list() + signer_infos.append(signer_info) + auth_info = AuthInfo( + signer_infos=signer_infos, + fee=Fee(amount=fee, gas_limit=gas_limit), + ) + + tx_body = TxBody() + tx_body.memo = memo + tx_body.messages.extend(packed_msgs) + multi_signature = MultiSignature(signatures=multi_sign_data.signatures) + + tx = Tx(body=tx_body, auth_info=auth_info, signatures=[multi_signature.SerializeToString()]) + return tx diff --git a/example/multi_sign/config.yaml b/example/multi_sign/config.yaml new file mode 100644 index 0000000..cca3861 --- /dev/null +++ b/example/multi_sign/config.yaml @@ -0,0 +1,19 @@ +chain_id_test: + validators: + - coins: 10000cro + staked: 1000cro + - coins: 1000cro + staked: 1000cro + accounts: + - name: msigner0 + coins: 2000cro + mnemonic: hurry exist clerk safe aware anchor brush run dentist come surge frame tired economy school grief volcano enforce word alpha liar clever sure taxi + - name: msigner1 + coins: 2000cro + - name: msigner2 + coins: 2000cro + mnemonic: repeat life corn cliff tragic merry zoo saddle fuel shove column pulp decorate forward rabbit ocean agent snack gaze mansion when wood grab pear + app_state: + staking: + params: + unbonding_time: "10s" diff --git a/example/multi_sign/main.py b/example/multi_sign/main.py new file mode 100644 index 0000000..fd42689 --- /dev/null +++ b/example/multi_sign/main.py @@ -0,0 +1,195 @@ +import time +from pathlib import Path + +import requests +import yaml +from google.protobuf.any_pb2 import Any as ProtoAny +from pystarport.cluster import ClusterCLI, init_cluster, interact, start_cluster +from pystarport.ports import api_port +from pystarport.proto_python.api_util import ApiUtil + +from chainlibpy.amino import Coin, StdFee +from chainlibpy.amino.message import MsgSend +from chainlibpy.amino.transaction import Transaction +from chainlibpy.generated.cosmos.base.v1beta1.coin_pb2 import Coin as ProtoCoin +from chainlibpy.generated.cosmos.crypto.multisig.keys_pb2 import LegacyAminoPubKey +from chainlibpy.generated.cosmos.crypto.secp256k1.keys_pb2 import PubKey as ProtoPubKey +from chainlibpy.grpc_client import ( + GrpcClient, + NetworkConfig, +) +from chainlibpy.transaction import gen_multi_tx, gen_packed_send_msg +from chainlibpy.multisign.signature import SingleSignatureV2 +from chainlibpy.wallet import Wallet + + +def packed_to_any(m): + packed = ProtoAny() + packed.Pack(m, type_url_prefix="/") + return packed + + +class ChainMainClient(object): + ''' + we use pystarport to create the IBC env + need to install hermes: https://github.com/informalsystems/ibc-rs/releases + ''' + + def __init__(self, data_root=Path("./data"), config_file="config.yaml", chain_id="chain_id_test"): + self.data_root = data_root + self.config_file = config_file + self.chain_id = chain_id + + @property + def cluster(self): + config = yaml.safe_load(open(self.config_file)) + clis = {} + for key in config: + if key == "relayer": + continue + chain_id = key + clis[chain_id] = ClusterCLI(self.data_root, chain_id=chain_id) + return clis[self.chain_id] + + def url_base(self, index=0): + cli = self.cluster + port = api_port(cli.base_port(index)) + return "http://127.0.0.1:{}".format(port) + + @property + def api(self): + index = 0 + cli = self.cluster + port = api_port(cli.base_port(index)) + return ApiUtil(port) + + def get_balance(self, address): + url_base = self.url_base(index=0) + url_balance = f"{url_base}/cosmos/bank/v1beta1/balances/{address}" + print(url_balance) + response = requests.get(url_balance) + balance = int(response.json()["balances"][0]["amount"]) + return balance + + def get_account_info(self, address): + account_info = self.api.account_info(address) + account_num = int(account_info["account_num"]) + sequence = int(account_info["sequence"]) + return account_num, sequence + + def send_tx(self, data): + url_base = self.url_base() + print(url_base) + url = f"{url_base}/cosmos/tx/v1beta1/txs" + response = requests.post(url, json=data) + return response + + def start(self): + ''' + after start the tasks, you can use `supervisorctl -c task.ini` to see the status of each program + ''' + interact(f"rm -r {self.data_root}; mkdir -p {self.data_root}", ignore_error=True) + data_dir = Path(self.data_root) + init_cluster(data_dir, "config.yaml", 26650) + start_cluster(data_dir) + +# the wallets which create the multi wallet +SEED_0 = "hurry exist clerk safe aware anchor brush run dentist come surge frame tired economy school grief volcano enforce word alpha liar clever sure taxi" +SEED_1 = "repeat life corn cliff tragic merry zoo saddle fuel shove column pulp decorate forward rabbit ocean agent snack gaze mansion when wood grab pear" +MULTI_WALLET = "multi_wallet" + + +def prepare(): + client = ChainMainClient() + # start the chain-maind + # client.start() + # 1. create multi wallet + cluster = client.cluster + try: + cluster.make_multisig(MULTI_WALLET, "msigner0", "msigner1") + except: + pass + multi_addr = cluster.address(MULTI_WALLET) + + # 2. send some coin to multi wallet + wallet_0 = Wallet(SEED_0) + cluster.transfer(wallet_0.address, multi_addr, "500basecro") + time.sleep(3) + balance = client.get_balance(multi_addr) + print("multi address balance: ", balance) + return multi_addr + + +def main(): + chain_id = "chain_id_test" + wallet_0 = Wallet(SEED_0) + wallet_1 = Wallet(SEED_1) + chain_maind_client = ChainMainClient(chain_id=chain_id) + # multi_address = prepare() + # print(multi_address) + multi_address = "cro12s2gcgzxg7ugucmeu575rmtszrqf2y5xtaug6x" + to_address = "cro1hk220qwxp0c8m3pzazardmmfv8y0mg7ukdnn37" + + # print multi wallet balance + balance = chain_maind_client.get_balance(multi_address) + print("multi address balance: ", balance) + + fee = StdFee(gas="2000", amount=[]) + + config = NetworkConfig( + grpc_endpoint="0.0.0.0:26653", + chain_id=chain_id, + address_prefix="cro", + coin_denom="cro", + coin_base_denom="basecro", + derivation_path="m/44'/394'/0'/0/0", + exponent=8, + ) + + grpc_client = GrpcClient(config) + res = grpc_client.query_account_balance(multi_address) + print(f"get multi address balance: {res.balance.amount}") + account_info = grpc_client.query_account(multi_address) + account_num = account_info.account_number + sequence = account_info.sequence + print(f"account_num {account_num}, sequence: {sequence}") + # account_num, sequence = 12, 0 + + amount = "100" + msg = MsgSend(from_address=multi_address, to_address=to_address, amount=[Coin(amount)]) + + # first make two single amino transaction, and create single_sig_v2 + sig_batch = list() + threshold = 2 + + any_pubkeys = [packed_to_any(ProtoPubKey(key=w.public_key)) for w in [wallet_0, wallet_1]] + multi_pubkey = LegacyAminoPubKey(threshold=threshold, public_keys=any_pubkeys) + for index, wallet in enumerate([wallet_0, wallet_1]): + tx = Transaction( + wallet=wallet, + account_num=account_num, + sequence=sequence, + chain_id=chain_id, + fee=fee, + multi_sign_address=multi_address + ) + tx.add_msg(msg) + single_sig_v2 = SingleSignatureV2(wallet.public_key, sequence, tx.sign()) + sig_batch.append(single_sig_v2) + + amount = [ProtoCoin(amount="10000", denom="basecro")] + msg = gen_packed_send_msg(multi_address, to_address, amount) + tx = gen_multi_tx( + [msg], + multi_pubkey, + sig_batch, + sequence, + ) + import pdb; pdb.set_trace() + response = grpc_client.broadcast_transaction(tx.SerializeToString()) + print(f"send tx response: {response}") + + +if __name__ == "__main__": + prepare() + main() diff --git a/generate_protos.sh b/generate_protos.sh index fe5f244..f782000 100755 --- a/generate_protos.sh +++ b/generate_protos.sh @@ -29,6 +29,7 @@ $COSMOS_SDK_DIR/proto/cosmos/base/query/v1beta1/pagination.proto $COSMOS_SDK_DIR/proto/cosmos/base/v1beta1/coin.proto $COSMOS_SDK_DIR/proto/cosmos/crypto/secp256k1/keys.proto $COSMOS_SDK_DIR/proto/cosmos/crypto/multisig/v1beta1/multisig.proto +$COSMOS_SDK_DIR/proto/cosmos/crypto/multisig/keys.proto $COSMOS_SDK_DIR/proto/cosmos/tx/signing/v1beta1/signing.proto $COSMOS_SDK_DIR/proto/cosmos/tx/v1beta1/service.proto $COSMOS_SDK_DIR/proto/cosmos/tx/v1beta1/tx.proto