├── .gitignore ├── LICENSE ├── README.md ├── bech32m.py └── fuel_wallet.py /.gitignore: -------------------------------------------------------------------------------- 1 | ### AL ### 2 | #Template for AL projects for Dynamics 365 Business Central 3 | #launch.json folder 4 | .vscode/ 5 | #Cache folder 6 | .alcache/ 7 | #Symbols folder 8 | .alpackages/ 9 | #Snapshots folder 10 | .snapshots/ 11 | #Testing Output folder 12 | .output/ 13 | #Extension App-file 14 | *.app 15 | #Rapid Application Development File 16 | rad.json 17 | #Translation Base-file 18 | *.g.xlf 19 | #License-file 20 | *.flf 21 | #Test results file 22 | TestResults.xml 23 | 24 | venv 25 | __pycache__ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 guo shawn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fuelwallet-py 2 | python for blockchain fuel official wallet, suport use mnemonic seeds generate fuel address and private key 3 | 4 | ## pip 5 | ``` 6 | pip install bip_utils 7 | ``` 8 | 9 | ``` 10 | # use this project dir`s bech32m.py 11 | from bech32m import encode # a little different with bitcoin bech32m 12 | ``` 13 | 14 | ## 15 | ```python 16 | from hashlib import sha256 17 | from bip_utils import Bip39SeedGenerator 18 | from bip_utils import Bip32Secp256k1 19 | from bech32m import encode # a little different with bitcoin bech32m 20 | 21 | class FuelWallet(): 22 | 23 | def __init__(self, mnemonic, password='', wallet_index=0) -> None: 24 | 25 | self.mnemonic = mnemonic.strip() 26 | self.password = password # if have password 27 | self.derive_default_path = f"m/44'/1179993420'/{wallet_index}'/0/0" 28 | self.prefix = 'fuel' 29 | 30 | def get_address_pk(self): 31 | seed_bytes = Bip39SeedGenerator(self.mnemonic).Generate(self.password) 32 | bip32_mst_ctx = Bip32Secp256k1.FromSeed(seed_bytes) 33 | bip32_der_ctx = bip32_mst_ctx.DerivePath(self.derive_default_path) 34 | 35 | pk: bytes = bip32_der_ctx.PrivateKey().Raw().ToBytes() 36 | extended_key = Bip32Secp256k1.FromPrivateKey(pk) 37 | pubkey = extended_key.PublicKey().RawUncompressed().ToBytes().hex()[2:] 38 | pubkey_bytes = bytes.fromhex(pubkey) 39 | sha256_bytes = sha256(pubkey_bytes).digest() 40 | address = encode(self.prefix, sha256_bytes) 41 | 42 | return pk.hex(), address 43 | ``` 44 | 45 | ## test 46 | ```python 47 | if __name__ == '__main__': 48 | 49 | mnemonic = 'seek clean tell token spread parrot pear tray beef desk sponsor plate' 50 | print(f'mnemonic seeds: {mnemonic}') 51 | for wallet_index in range(5): 52 | fl = FuelWallet(mnemonic=mnemonic, wallet_index=wallet_index) 53 | 54 | pk, address = fl.get_address_pk() 55 | print(f'address index {wallet_index}, address: {address}, pk: 0x{pk}') 56 | ``` 57 | ## result 58 | 59 | ``` 60 | mnemonic seeds: seek clean tell token spread parrot pear tray beef desk sponsor plate 61 | address index 0, address: fuel1gu47yf32mq2khczewvw04el088y34f49fh3vqp4vn8p9yrc28uaqrr3t85, pk: 0x1ef91ec4b2a39d652091f6f217029f5a33eea7e9913da4fa26eb0a79d6663bee 62 | address index 1, address: fuel1pqkzasvy0x2vpvn3humwyq492ccrgqt9t0mvlpdnpkw09tnu9u9sn7hrcq, pk: 0xa9da58f2169d88ea98fff6367c7c6fdcb153c3eef5d8d07881e5f10a8fe55e1a 63 | address index 2, address: fuel142lr9rsntee7lnsxvck7m49fdpfca3vvcmqqvvtfzqwtuuge2qdq5gj259, pk: 0x6af82b17141a6793bc7fb703e98a256e1a446ce0e03c1d8884e3592ad21333a2 64 | address index 3, address: fuel1n0zstx2dntgp64v29wgzsqc4jumcgtse30ws4s3zphpn8rjhzs5s3ttfyf, pk: 0x4423c07fc04d7d73ff34f46bc6b652ed759896bdb689fff1930fc4de98e82d53 65 | address index 4, address: fuel1vqn9mu84v8keec0u8fge8295epr5mn6c74nwekyqn5yspgn8hqdqsg6ryh, pk: 0x0056a68f643de783298adf4ca3269a15110fb02a52937adeef36f53f43ed0b72 66 | ``` 67 | 68 | ## import the mnemonic seed to fuel official wallet , get result 69 | 70 | 71 | 72 | ## if you export private key from wallet, you will get same result 73 | 74 | 75 | # last but important! 76 | 1. test the result and compare it with main web wallet app(such as: metamask, mathwallet, trustwallet...) before you deposit crypto assets to the address 77 | 2. some wallet may get diffrent result, because it may use diffrent derive path to generate wallet 78 | 3. learn about hd-wallet principle by your self 79 | -------------------------------------------------------------------------------- /bech32m.py: -------------------------------------------------------------------------------- 1 | # License for original (reference) implementation: 2 | # 3 | # Copyright (c) 2017, 2020 Pieter Wuille 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | """Reference implementation for Bech32/Bech32m and segwit addresses.""" 24 | 25 | 26 | from collections.abc import ByteString 27 | from enum import Enum 28 | from typing import NamedTuple 29 | 30 | 31 | class Encoding(Enum): 32 | """Enumeration type to list the various supported encodings.""" 33 | 34 | BECH32 = 1 35 | BECH32M = 2 36 | 37 | 38 | CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" 39 | BECH32M_CONST = 0x2BC830A3 40 | 41 | 42 | class DecodeError(ValueError): 43 | pass 44 | 45 | 46 | class HrpDoesNotMatch(DecodeError): 47 | pass 48 | 49 | 50 | class DecodedAddress(NamedTuple): 51 | witver: int 52 | witprog: bytes 53 | 54 | 55 | def bech32_polymod(values: ByteString) -> int: 56 | """Internal function that computes the Bech32 checksum.""" 57 | generator = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3] 58 | chk = 1 59 | for value in values: 60 | top = chk >> 25 61 | chk = (chk & 0x1FFFFFF) << 5 ^ value 62 | for i in range(5): 63 | chk ^= generator[i] if ((top >> i) & 1) else 0 64 | return chk 65 | 66 | 67 | def bech32_hrp_expand(hrp: str) -> bytes: 68 | """Expand the HRP into values for checksum computation.""" 69 | return bytes([ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]) 70 | 71 | 72 | def bech32_verify_checksum(hrp: str, data: bytes) -> Encoding: 73 | """Verify a checksum given HRP and converted data characters.""" 74 | const = bech32_polymod(bech32_hrp_expand(hrp) + data) 75 | if const == 1: 76 | return Encoding.BECH32 77 | if const == BECH32M_CONST: 78 | return Encoding.BECH32M 79 | # Invalid checksum 80 | raise DecodeError() 81 | 82 | 83 | def bech32_create_checksum(hrp: str, data: bytes, spec: Encoding) -> bytes: 84 | """Compute the checksum values given HRP and data.""" 85 | values = bech32_hrp_expand(hrp) + data 86 | const = BECH32M_CONST if spec == Encoding.BECH32M else 1 87 | polymod = bech32_polymod(values + bytes(6)) ^ const 88 | return bytes((polymod >> 5 * (5 - i)) & 31 for i in range(6)) 89 | 90 | 91 | def bech32_encode(hrp: str, data: bytes, spec: Encoding) -> str: 92 | """Compute a Bech32 string given HRP and data values.""" 93 | combined = data + bech32_create_checksum(hrp, data, spec) 94 | return hrp + "1" + "".join([CHARSET[d] for d in combined]) 95 | 96 | 97 | def bech32_decode(bech: str) -> tuple[str, memoryview, Encoding]: 98 | """Validate a Bech32/Bech32m string, and determine HRP and data.""" 99 | if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or ( 100 | bech.lower() != bech and bech.upper() != bech 101 | ): 102 | # HRP character out of range 103 | raise DecodeError() 104 | bech = bech.lower() 105 | pos = bech.rfind("1") 106 | if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: 107 | # No separator character / Empty HRP / overall max length exceeded 108 | raise DecodeError() 109 | if not all(x in CHARSET for x in bech[pos + 1 :]): 110 | # Invalid data character 111 | raise DecodeError() 112 | hrp = bech[:pos] 113 | data = memoryview(bytes(CHARSET.find(x) for x in bech[pos + 1 :])) 114 | spec = bech32_verify_checksum(hrp, data) 115 | return (hrp, data[:-6], spec) 116 | 117 | 118 | def convertbits(data: ByteString, frombits: int, tobits: int, pad: bool = True) -> bytearray: 119 | """General power-of-2 base conversion.""" 120 | acc = 0 121 | bits = 0 122 | ret = bytearray() 123 | maxv = (1 << tobits) - 1 124 | max_acc = (1 << (frombits + tobits - 1)) - 1 125 | for value in data: 126 | if value < 0 or (value >> frombits): 127 | # XXX Not covered by tests 128 | raise DecodeError() 129 | acc = ((acc << frombits) | value) & max_acc 130 | bits += frombits 131 | while bits >= tobits: 132 | bits -= tobits 133 | ret.append((acc >> bits) & maxv) 134 | if pad: 135 | if bits: 136 | ret.append((acc << (tobits - bits)) & maxv) 137 | elif bits >= frombits or ((acc << (tobits - bits)) & maxv): 138 | # More than 4 padding bits / Non-zero padding in 8-to-5 conversion 139 | raise DecodeError() 140 | return ret 141 | 142 | 143 | def decode(hrp: str, addr: str) -> DecodedAddress: 144 | """Decode a segwit address.""" 145 | hrpgot, data, spec = bech32_decode(addr) 146 | if hrpgot != hrp: 147 | raise HrpDoesNotMatch() 148 | witprog = convertbits(data[1:], 5, 8, False) 149 | if len(witprog) < 2 or len(witprog) > 40: 150 | # Invalid program length 151 | raise DecodeError() 152 | witver = data[0] 153 | if witver > 16: 154 | # Invalid witness version 155 | raise DecodeError() 156 | if witver == 0 and len(witprog) != 20 and len(witprog) != 32: 157 | # Invalid program length for witness version 0 (per BIP141) 158 | raise DecodeError() 159 | if witver == 0 and spec != Encoding.BECH32 or witver != 0 and spec != Encoding.BECH32M: 160 | # Invalid checksum algorithm 161 | raise DecodeError() 162 | return DecodedAddress(witver, witprog) 163 | 164 | 165 | def encode(hrp: str, witprog: ByteString, isbech32m=True) -> str: 166 | """Encode a segwit address.""" 167 | spec = Encoding.BECH32 if not isbech32m else Encoding.BECH32M 168 | conve_bytesarray = convertbits(witprog, 8, 5) 169 | ret = bech32_encode(hrp, conve_bytesarray, spec) 170 | return ret 171 | -------------------------------------------------------------------------------- /fuel_wallet.py: -------------------------------------------------------------------------------- 1 | from hashlib import sha256 2 | from bip_utils import Bip39SeedGenerator 3 | from bip_utils import Bip32Secp256k1 4 | from bech32m import encode 5 | 6 | 7 | class FuelWallet(): 8 | 9 | def __init__(self, mnemonic, password='', wallet_index=0) -> None: 10 | 11 | self.mnemonic = mnemonic.strip() 12 | self.password = password # if have password 13 | self.derive_default_path = f"m/44'/1179993420'/{wallet_index}'/0/0" 14 | self.prefix = 'fuel' 15 | 16 | def get_address_pk(self): 17 | seed_bytes = Bip39SeedGenerator(self.mnemonic).Generate(self.password) 18 | bip32_mst_ctx = Bip32Secp256k1.FromSeed(seed_bytes) 19 | bip32_der_ctx = bip32_mst_ctx.DerivePath(self.derive_default_path) 20 | 21 | pk: bytes = bip32_der_ctx.PrivateKey().Raw().ToBytes() 22 | extended_key = Bip32Secp256k1.FromPrivateKey(pk) 23 | pubkey = extended_key.PublicKey().RawUncompressed().ToBytes().hex()[2:] 24 | pubkey_bytes = bytes.fromhex(pubkey) 25 | sha256_bytes = sha256(pubkey_bytes).digest() 26 | address = encode(self.prefix, sha256_bytes) 27 | 28 | return pk.hex(), address 29 | 30 | 31 | 32 | if __name__ == '__main__': 33 | 34 | mnemonic = 'seek clean tell token spread parrot pear tray beef desk sponsor plate' 35 | 36 | for wallet_index in range(5): 37 | fl = FuelWallet(mnemonic=mnemonic, wallet_index=wallet_index) 38 | 39 | pk, address = fl.get_address_pk() 40 | print(f'address: {address}, pk: 0x{pk}') 41 | --------------------------------------------------------------------------------