Skip to content

Commit

Permalink
V4.1.0
Browse files Browse the repository at this point in the history
- Added support for Sui addresses using Secp256k1, Secp256r1, Ed25519, and MultiSig.
- Implemented support for Sui BIP44 coin derivation.
  • Loading branch information
mrtnetwork committed Feb 6, 2025
1 parent cf834bb commit d893b34
Show file tree
Hide file tree
Showing 74 changed files with 2,386 additions and 417 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
## 4.1.0

- Added support for Sui addresses using Secp256k1, Secp256r1, Ed25519, and MultiSig.
- Implemented support for Sui BIP44 coin derivation.


## 4.0.1

- Fix schnorr vrf verification.
- Fix schnorr vrf verification.

## 4.0.0

Expand Down
8 changes: 2 additions & 6 deletions lib/bip/address/addr_dec_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,12 @@ class AddrDecUtils {
}

/// Validate and remove prefix from an address.
static String validateAndRemovePrefix(
String addr,
String prefix,
) {
static String validateAndRemovePrefix(String addr, String prefix) {
final prefixGot = addr.substring(0, prefix.length);

if (prefix != prefixGot) {
throw AddressConverterException(
'Invalid prefix (expected $prefix, got $prefixGot)',
);
'Invalid prefix (expected $prefix, got $prefixGot)');
}

return addr.substring(prefix.length);
Expand Down
325 changes: 265 additions & 60 deletions lib/bip/address/aptos_addr.dart
Original file line number Diff line number Diff line change
@@ -1,89 +1,294 @@
import 'package:blockchain_utils/bip/address/addr_dec_utils.dart';
import 'package:blockchain_utils/bip/address/decoder.dart';
import 'package:blockchain_utils/bip/address/encoder.dart';
import 'package:blockchain_utils/bip/coin_conf/constant/coins_conf.dart';
import 'package:blockchain_utils/bip/address/exception/exception.dart';
import 'package:blockchain_utils/bip/bip.dart';
import 'package:blockchain_utils/crypto/quick_crypto.dart';
import 'package:blockchain_utils/layout/layout.dart';
import 'package:blockchain_utils/utils/utils.dart';

import 'addr_key_validator.dart';

/// Constants related to Aptos blockchain addresses.
class AptosAddrConst {
/// The suffix byte used for single signature addresses.
static final singleSigSuffixByte = List<int>.from([0x00]);
static const int specialAddressLastBytesMax = 16;

/// aptos address bytes length
static const int addressBytesLength = 32;

/// The Ed25519 signing scheme flag
static const int ed25519AddressFlag = 0;

/// The multi-signature Ed25519 scheme flag
static const int multiEd25519AddressFlag = 1;

/// A single key signing scheme flag (likely used for single-key accounts)
static const int signleKeyAddressFlag = 2;

/// A multi-key signing scheme flag where multiple different types of keys are involved
static const int multikeyAddressFlag = 3;

/// Max number of keys in the multi-signature account
static const int maximumPublicKeys = 32;

/// Minimum number of keys required
static const int minPublicKeys = 2;

/// Minimum threshold of required signatures
static const int minthreshold = 1;

static const int shortAddressLength = 63;

/// Bcs layout for encdoing multikey address
static final multiKeyAddressLayout = LayoutConst.struct([
LayoutConst.bcsVector(
LayoutConst.bcsLazyEnum([
LazyVariantModel(
layout: LayoutConst.bcsBytes,
property: EllipticCurveTypes.ed25519.name,
index: 0),
LazyVariantModel(
layout: LayoutConst.bcsBytes,
property: EllipticCurveTypes.secp256k1.name,
index: 1)
], property: "pubKey"),
property: 'publicKeys'),
LayoutConst.u8(property: "requiredSignature")
]);

/// Bcs layout for encoding single key address
static final singleKeyAddressLayout = LayoutConst.bcsLazyEnum([
LazyVariantModel(
layout: LayoutConst.bcsBytes,
property: EllipticCurveTypes.ed25519.name,
index: 0),
LazyVariantModel(
layout: LayoutConst.bcsBytes,
property: EllipticCurveTypes.secp256k1.name,
index: 1)
]);
}

class AptosAddressUtils {
/// check address bytes and convert special address to 32bytes.
static List<int> praseAddressBytes(List<int> bytes) {
final length = bytes.length;
if (length != AptosAddrConst.addressBytesLength) {
throw AddressConverterException("Invalid aptos address bytes length.",
details: {
"expected": AptosAddrConst.addressBytesLength,
"length": bytes.length
});
}

return bytes;
}

/// convert address string to bytes with padding zero for special addresses.
static List<int> addressToBytes(String address) {
address = StringUtils.strip0x(address);
List<int>? bytes = BytesUtils.tryFromHexString(address,
paddingZero: address.length == 1 ||
address.length == AptosAddrConst.shortAddressLength);
if (bytes == null ||
(bytes.length != AptosAddrConst.addressBytesLength &&
bytes.length != 1)) {
throw AddressConverterException("Invalid aptos address.",
details: {"address": address});
}
if (bytes.length == 1) {
final byte = bytes[0];
if (byte >= AptosAddrConst.specialAddressLastBytesMax) {
throw AddressConverterException("Invalid special address.",
details: {"address": BytesUtils.toHexString(bytes)});
}
bytes = List.filled(AptosAddrConst.addressBytesLength, 0);
bytes.last = byte;
}
return praseAddressBytes(bytes);
}

/// convert bytes (ED25519, Secp256k1 or multisig key data) to address with specify scheme
static List<int> hashKeyBytes(
{required List<int> bytes, required int scheme}) {
bytes = [...bytes, scheme];
bytes = QuickCrypto.sha3256Hash(bytes);
return bytes;
}

/// encode ED25519 public key to address
static List<int> encodeEd25519Key(List<int> bytes) {
try {
final key = AddrKeyValidator.validateAndGetEd25519Key(bytes)
.compressed
.sublist(1);
return hashKeyBytes(
bytes: key, scheme: AptosAddrConst.ed25519AddressFlag);
} catch (e) {
throw AddressConverterException(
"Failed to generate Aptos address: Invalid Ed25519 public key provided.");
}
}

/// encode public key to SignleKey address
static List<int> encodeSingleKey(IPublicKey publicKey) {
try {
final pubkeyBytes = switch (publicKey.curve) {
EllipticCurveTypes.secp256k1 => publicKey.uncompressed,
EllipticCurveTypes.ed25519 => publicKey.compressed.sublist(1),
_ => throw AddressConverterException(
"Unsupported public key: Aptos SingleKey can only be generated from secp256k1 or ed25519 public keys.")
};
final structLayout = {publicKey.curve.name: pubkeyBytes};
final encode =
AptosAddrConst.singleKeyAddressLayout.serialize(structLayout);
return hashKeyBytes(
bytes: encode, scheme: AptosAddrConst.signleKeyAddressFlag);
} on AddressConverterException {
rethrow;
} catch (e) {
throw AddressConverterException("Invalid aptos MultiKey address bytes.",
details: {"error": e.toString()});
}
}

/// encode Multi ED25519 public keys to MultiEd25519 address
static List<int> encodeMultiEd25519Key(
List<Ed25519PublicKey> publicKeys, int threshold) {
try {
if (publicKeys.length < AptosAddrConst.minPublicKeys ||
publicKeys.length > AptosAddrConst.maximumPublicKeys) {
throw AddressConverterException(
"The number of public keys provided is invalid. It must be between ${AptosAddrConst.minPublicKeys} and ${AptosAddrConst.maximumPublicKeys}.");
}
if (threshold < AptosAddrConst.minthreshold ||
threshold > publicKeys.length) {
throw AddressConverterException(
"Invalid threshold. The threshold must be between ${AptosAddrConst.minthreshold} and the number of provided public keys (${publicKeys.length}).");
}
final keyBytes = [
...publicKeys.map((e) => e.compressed.sublist(1)).expand((e) => e),
threshold
];
return hashKeyBytes(
bytes: keyBytes, scheme: AptosAddrConst.multiEd25519AddressFlag);
} on AddressConverterException {
rethrow;
} catch (e) {
throw AddressConverterException(
"Invalid aptos MultiEd25519 address bytes.",
details: {"error": e.toString()});
}
}

/// encode Multi Public keys to MultiKey address
static List<int> encodeMultiKey(
List<IPublicKey> publicKeys, int requiredSignature) {
try {
final pubkeyLayoutStruct = publicKeys.map((e) {
return switch (e.curve) {
EllipticCurveTypes.secp256k1 => {e.curve.name: e.uncompressed},
EllipticCurveTypes.ed25519 => {e.curve.name: e.compressed.sublist(1)},
_ => throw AddressConverterException(
"Unsupported public key: Aptos Multikey address can only be generated from secp256k1 or ed25519 public keys.")
};
}).toList();
if (publicKeys.length < AptosAddrConst.minPublicKeys ||
publicKeys.length > AptosAddrConst.maximumPublicKeys) {
throw AddressConverterException(
"The number of public keys provided is invalid. It must be between ${AptosAddrConst.minPublicKeys} and ${AptosAddrConst.maximumPublicKeys}.");
}
if (requiredSignature < AptosAddrConst.minthreshold ||
requiredSignature > publicKeys.length) {
throw AddressConverterException(
"Invalid threshold. The threshold must be between ${AptosAddrConst.minthreshold} and the number of provided public keys (${publicKeys.length}).");
}
final layoutStruct = {
"requiredSignature": requiredSignature,
"publicKeys": pubkeyLayoutStruct
};
final encode =
AptosAddrConst.multiKeyAddressLayout.serialize(layoutStruct);
return hashKeyBytes(
bytes: encode, scheme: AptosAddrConst.multikeyAddressFlag);
} on AddressConverterException {
rethrow;
} catch (e) {
throw AddressConverterException("Invalid aptos MultiKey address bytes.",
details: {"error": e.toString()});
}
}
}

/// Implementation of the [BlockchainAddressDecoder] for Aptos address.
class AptosAddrDecoder implements BlockchainAddressDecoder {
/// Decode an Aptos blockchain address from its string representation.
///
/// This method decodes the provided `addr` string by removing the prefix,
/// ensuring the address length is valid, and parsing the hexadecimal string
/// to obtain the address bytes.
///
/// Parameters:
/// - `addr`: The Aptos blockchain address in string format.
/// - `kwargs` (optional): Additional arguments, though none are used in this method.
///
/// Returns:
/// - A List containing the decoded address bytes.
///
/// Throws:
/// - ArgumentException if the provided string is not a valid hex encoding.
///
/// This method is used to convert an Aptos blockchain address from its string
/// representation to its binary format for further processing.
@override
List<int> decodeAddr(String addr, [Map<String, dynamic> kwargs = const {}]) {
String addrNoPrefix = AddrDecUtils.validateAndRemovePrefix(
addr,
CoinsConf.aptos.params.addrPrefix!,
);
addrNoPrefix = addrNoPrefix.padLeft(QuickCrypto.sha3256DigestSize * 2, "0");
AddrDecUtils.validateLength(
addrNoPrefix, QuickCrypto.sha3256DigestSize * 2);

return BytesUtils.fromHexString(addrNoPrefix);
return AptosAddressUtils.addressToBytes(addr);
}
}

class AptosSingleKeyEd25519AddrEncoder implements BlockchainAddressEncoder {
/// This method is used to create an Aptos `SingleKey` address from `ED25519` public key.
@override
String encodeKey(List<int> pubKey, [Map<String, dynamic> kwargs = const {}]) {
final publicKey = AddrKeyValidator.validateAndGetEd25519Key(pubKey);
final addressBytes = AptosAddressUtils.encodeSingleKey(publicKey);

/// Concatenate the address prefix and the hash bytes, removing leading zeros
return BytesUtils.toHexString(addressBytes,
prefix: CoinsConf.aptos.params.addrPrefix);
}
}

class AptosSingleKeySecp256k1AddrEncoder implements BlockchainAddressEncoder {
/// This method is used to create an Aptos `SingleKey` address from `Sec256k1` public key.
@override
String encodeKey(List<int> pubKey, [Map<String, dynamic> kwargs = const {}]) {
final publicKey = AddrKeyValidator.validateAndGetSecp256k1Key(pubKey);
final addressBytes = AptosAddressUtils.encodeSingleKey(publicKey);

/// Concatenate the address prefix and the hash bytes, removing leading zeros
return BytesUtils.toHexString(addressBytes,
prefix: CoinsConf.aptos.params.addrPrefix);
}
}

/// Implementation of the [BlockchainAddressEncoder] for Aptos address.
class AptosAddrEncoder implements BlockchainAddressEncoder {
/// Encode an Aptos blockchain address from a public key.
///
/// This method encodes an Aptos blockchain address from the provided `pubKey`
/// by performing the following steps:
/// 1. Validate the public key and extract its raw compressed bytes.
/// 2. Prepare the payload by appending a single-sig suffix byte.
/// 3. Compute the SHA-3-256 hash of the payload.
/// 4. Concatenate the address prefix and the hash bytes.
/// 5. Remove leading zeros from the resulting hex-encoded address.
///
/// Parameters:
/// - `pubKey`: The public key for which to generate the address.
/// - `kwargs` (optional): Additional arguments, though none are used in this method.
///
/// Returns:
/// - A hex-encoded string representing the generated Aptos blockchain address.
///
/// This method is used to create an Aptos blockchain address from a public key.
/// The resulting address is a hexadecimal string without leading zeros.
/// This method is used to create an Aptos `ED25519` address from public key.
@override
String encodeKey(List<int> pubKey, [Map<String, dynamic> kwargs = const {}]) {
final pubKeyBytes = pubKey;
final pubKeyObj = AddrKeyValidator.validateAndGetEd25519Key(pubKeyBytes);
final addressBytes = AptosAddressUtils.encodeEd25519Key(pubKey);

/// Prepare the payload by appending a single-sig suffix byte
final payloadBytes = List<int>.from([
...List<int>.from(pubKeyObj.compressed.sublist(1)),
...AptosAddrConst.singleSigSuffixByte
]);
return BytesUtils.toHexString(addressBytes,
prefix: CoinsConf.aptos.params.addrPrefix);
}

/// Compute the SHA-3-256 hash of the payload
final keyHashBytes = QuickCrypto.sha3256Hash(payloadBytes);
/// This method is used to create an Aptos `SingleKey` address from (ED25519, Sec256k1) public key.
String encodeSingleKey(IPublicKey pubKey) {
final addressBytes = AptosAddressUtils.encodeSingleKey(pubKey);

/// Concatenate the address prefix and the hash bytes, removing leading zeros
return CoinsConf.aptos.params.addrPrefix! +
BytesUtils.toHexString(keyHashBytes).replaceFirst(RegExp('^0+'), '');
return BytesUtils.toHexString(addressBytes,
prefix: CoinsConf.aptos.params.addrPrefix);
}

/// This method is used to create an Aptos `MultiEd25519` address from ED25519 public keys.
String encodeMultiEd25519Key(
{required List<Ed25519PublicKey> publicKeys, required int threshold}) {
final addressBytes =
AptosAddressUtils.encodeMultiEd25519Key(publicKeys, threshold);
return BytesUtils.toHexString(addressBytes,
prefix: CoinsConf.aptos.params.addrPrefix);
}

/// This method is used to create an Aptos `MultiKey` address from (ED25519 or Secp256k1) public keys.
String encodeMultiKey(
{required List<IPublicKey> publicKeys, required int requiredSignature}) {
final addressBytes =
AptosAddressUtils.encodeMultiKey(publicKeys, requiredSignature);
return BytesUtils.toHexString(addressBytes,
prefix: CoinsConf.aptos.params.addrPrefix);
}
}
4 changes: 4 additions & 0 deletions lib/bip/address/decoders.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ library;
/// Export statement for Ada Byron address decoder.
export 'ada/ada_byron_addr.dart' show AdaByronAddrDecoder;

/// Export statement for Sui address decoder.
export 'sui.dart'
show SuiAddrEncoder, SuiAddressUtils, SuiPublicKeyAndWeight, SuiAddrConst;

/// Export statements for Ada Shelley address decoders.
export 'ada/ada.dart'
show
Expand Down
Loading

0 comments on commit d893b34

Please sign in to comment.