Skip to content

Commit

Permalink
feat: add shelley address & cardano serialization lib (#478)
Browse files Browse the repository at this point in the history
* feat: add shelley address & cardano serialization lib

* fix: address to string

* style: disable cspell for cardano addresses

---------

Co-authored-by: Dominik Toton <[email protected]>
  • Loading branch information
dtscalac and Dominik Toton authored May 14, 2024
1 parent 1148578 commit 6d985bb
Show file tree
Hide file tree
Showing 20 changed files with 931 additions and 1 deletion.
9 changes: 8 additions & 1 deletion .config/dictionaries/project.dic
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,11 @@ xcodeproj
xctest
xctestrun
xcworkspace
yoroi
yoroi
multiplatform
Multiplatform
Easterling
lovelace
lovelaces
pinenacl
dtscalac
2 changes: 2 additions & 0 deletions catalyst_voices/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ environment:
dependencies:
animated_text_kit: ^4.2.2
animations: ^2.0.11
catalyst_cardano_serialization:
path: ../catalyst_voices_packages/catalyst_cardano_serialization
catalyst_voices_assets:
path: ./packages/catalyst_voices_assets
catalyst_voices_blocs:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# 0.1.0

* Initial release.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# catalyst_cardano_serialization
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
include: package:catalyst_analysis/analysis_options.1.0.0.yaml

analyzer:
exclude: [build/**, lib/*.g.dart, lib/generated/**]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export 'src/address.dart';
export 'src/exceptions.dart';
export 'src/fees.dart';
export 'src/hashes.dart';
export 'src/transaction.dart';
export 'src/types.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright 2021 Richard Easterling
// SPDX-License-Identifier: Apache-2.0

// ignore_for_file: avoid_equals_and_hash_code_on_mutable_classes

import 'package:bip32_ed25519/bip32_ed25519.dart';
import 'package:catalyst_cardano_serialization/src/exceptions.dart';
import 'package:catalyst_cardano_serialization/src/types.dart';
import 'package:cbor/cbor.dart';

/// [ShelleyAddress] supports bech32 encoded addresses as defined in CIP19.
class ShelleyAddress {
/// The prefix of a base address.
static const String defaultAddrHrp = 'addr';

/// The prefix of a stake/reward address.
static const String defaultRewardHrp = 'stake';

/// The hrp suffix of an address on testnet network.
static const String testnetHrpSuffix = '_test';

static const Bech32Encoder _mainNetEncoder =
Bech32Encoder(hrp: defaultAddrHrp);
static const Bech32Encoder _testNetEncoder =
Bech32Encoder(hrp: defaultAddrHrp + testnetHrpSuffix);
static const Bech32Encoder _mainNetRewardEncoder =
Bech32Encoder(hrp: defaultRewardHrp);
static const Bech32Encoder _testNetRewardEncoder =
Bech32Encoder(hrp: defaultRewardHrp + testnetHrpSuffix);

/// Raw bytes of address.
/// Format [ 8 bit header | payload ]
final Uint8List bytes;

/// The prefix specifying the address type and networkId.
final String hrp;

/// The constructor for [ShelleyAddress] from raw [bytes] and [hrp].
ShelleyAddress(List<int> bytes, {this.hrp = defaultAddrHrp})
: bytes = Uint8List.fromList(bytes);

/// The constructor which parses the address from bech32 format.
factory ShelleyAddress.fromBech32(String address) {
final hrp = _hrpPrefix(address);
if (hrp.isEmpty) {
throw InvalidAddressException(
'not a valid Bech32 address - no prefix: $address',
);
}

switch (hrp) {
case defaultAddrHrp:
return ShelleyAddress(_mainNetEncoder.decode(address), hrp: hrp);
case const (defaultAddrHrp + testnetHrpSuffix):
return ShelleyAddress(_testNetEncoder.decode(address), hrp: hrp);
case defaultRewardHrp:
return ShelleyAddress(_mainNetRewardEncoder.decode(address), hrp: hrp);
case const (defaultRewardHrp + testnetHrpSuffix):
return ShelleyAddress(_testNetRewardEncoder.decode(address), hrp: hrp);
default:
return ShelleyAddress(
Bech32Encoder(hrp: hrp).decode(address),
hrp: hrp,
);
}
}

/// Returns the [NetworkId] related to this address.
NetworkId get network => NetworkId.testnet.id == (bytes[0] & 0x0f)
? NetworkId.testnet
: NetworkId.mainnet;

/// Encodes the address in bech32 format.
String toBech32() {
final prefix = _computeHrp(network, hrp);
switch (prefix) {
case defaultAddrHrp:
return _mainNetEncoder.encode(bytes);
case const (defaultAddrHrp + testnetHrpSuffix):
return _testNetEncoder.encode(bytes);
case defaultRewardHrp:
return _mainNetRewardEncoder.encode(bytes);
case const (defaultRewardHrp + testnetHrpSuffix):
return _testNetRewardEncoder.encode(bytes);
default:
return Bech32Encoder(hrp: prefix).encode(bytes);
}
}

/// Serializes the type as cbor.
CborValue toCbor() => CborBytes(bytes);

@override
int get hashCode => Object.hash(bytes, hrp);

@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is! ShelleyAddress) return false;
if (bytes.length != other.bytes.length) return false;
if (hrp != other.hrp) return false;

for (var i = 0; i < bytes.length; i++) {
if (bytes[i] != other.bytes[i]) return false;
}
return true;
}

@override
String toString() => toBech32();

/// If were using the testnet, make sure the hrp ends with '_test'
static String _computeHrp(NetworkId id, String prefix) {
if (id == NetworkId.mainnet) {
return prefix;
} else if (prefix.endsWith(testnetHrpSuffix)) {
return prefix;
} else {
return prefix + testnetHrpSuffix;
}
}

static String _hrpPrefix(String addr) {
final s = addr.trim();
final i = s.indexOf('1');
return s.substring(0, i > 0 ? i : 0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/// Exception thrown when the transaction exceeds the allowed maximum size.
final class MaxTxSizeExceededException implements Exception {
/// The maximum amount of bytes per transaction.
final int maxTxSize;

/// The amount of bytes of transaction that exceeded it's maximum size.
final int actualTxSize;

/// The default constructor for [MaxTxSizeExceededException].
const MaxTxSizeExceededException({
required this.maxTxSize,
required this.actualTxSize,
});

@override
String toString() => 'MaxTxSizeExceededException('
'maxTxSize=$maxTxSize'
', actualTxSize:$actualTxSize'
')';
}

/// Exception thrown when building a transaction that doesn't specify the fee.
final class TxFeeNotSpecifiedException implements Exception {
/// The default constructor for [TxFeeNotSpecifiedException].
const TxFeeNotSpecifiedException();

@override
String toString() => 'TxFeeNotSpecifiedException';
}

/// Exception thrown when parsing a hash that has incorrect length.
final class HashFormatException implements Exception {
/// The default constructor for [HashFormatException].
const HashFormatException();

@override
String toString() => 'HashFormatException';
}

/// Exception thrown if the address doesn't match the bech32 specification
/// for Shelley addresses.
final class InvalidAddressException implements Exception {
/// Exception details.
final String message;

/// Default constructor [InvalidAddressException].
const InvalidAddressException(this.message);

@override
String toString() => 'InvalidAddressException: $message';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'package:catalyst_cardano_serialization/src/transaction.dart';
import 'package:catalyst_cardano_serialization/src/types.dart';
import 'package:cbor/cbor.dart';

/// Calculates fees for the transaction on Cardano blockchain.
///
/// The fee is calculated using the following formula:
/// - `fee = constant + tx.bytes.len * coefficient`
final class LinearFee {
/// The constant amount of [Coin] that is charged per transaction.
final Coin constant;

/// The amount of [Coin] per transaction byte that is charged per transaction.
final Coin coefficient;

/// The default constructor for the [LinearFee].
/// The parameters are Cardano protocol parameters.
const LinearFee({
required this.constant,
required this.coefficient,
});

/// Calculates the fee for the transaction denominated in lovelaces.
///
/// The formula doesn't take into account smart contract scripts.
Coin minNoScriptFee(Transaction tx) {
final bytesCount = cbor.encode(tx.toCbor()).length;
return Coin(bytesCount) * coefficient + constant;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// ignore_for_file: avoid_equals_and_hash_code_on_mutable_classes

import 'dart:typed_data';

import 'package:catalyst_cardano_serialization/src/exceptions.dart';
import 'package:catalyst_cardano_serialization/src/transaction.dart';
import 'package:cbor/cbor.dart';
import 'package:convert/convert.dart';
import 'package:pinenacl/digests.dart';

/// Implements a common base of hash types that holds
/// binary [bytes] of exact [length].
abstract base class BaseHash {
/// The raw [bytes] of a hash.
final List<int> bytes;

/// Constructs the [BaseHash] from raw [bytes].
BaseHash.fromBytes({required this.bytes}) {
if (bytes.length != length) {
throw const HashFormatException();
}
}

/// Constructs the [BaseHash] from a hex string representation
/// of [bytes].
BaseHash.fromHex(String string) : this.fromBytes(bytes: hex.decode(string));

/// The expected length of the transaction hash bytes.
int get length;

/// Serializes the type as cbor.
CborValue toCbor() => CborBytes(bytes);

/// Returns the hex string representation of [bytes].
String toHex() => hex.encode(bytes);

@override
String toString() => toHex();

@override
int get hashCode => Object.hash(bytes, length);

@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is! BaseHash) return false;

// prevent subclasses of different types to be equal to each other,
// even if they hold the same bytes they represent different kinds
if (other.runtimeType != runtimeType) return false;

if (length != other.length) return false;

for (var i = 0; i < bytes.length; i++) {
if (bytes[i] != other.bytes[i]) return false;
}

return true;
}
}

/// Describes the hash of the transaction which serves as proof
/// of transaction validation.
final class TransactionHash extends BaseHash {
static const int _length = 32;

/// Constructs the [TransactionHash] from raw [bytes].
TransactionHash.fromBytes({required super.bytes}) : super.fromBytes();

/// Constructs the [TransactionHash] from a hex string representation
/// of [bytes].
TransactionHash.fromHex(super.string) : super.fromHex();

@override
int get length => _length;
}

/// Describes the hash of auxiliary data which is included
/// in the transaction body.
final class AuxiliaryDataHash extends BaseHash {
static const int _length = 32;

/// Constructs the [AuxiliaryDataHash] from raw [bytes].
AuxiliaryDataHash.fromBytes({required super.bytes}) : super.fromBytes();

/// Constructs the [AuxiliaryDataHash] from a hex string representation
/// of [bytes].
AuxiliaryDataHash.fromHex(super.string) : super.fromHex();

/// Constructs the [AuxiliaryDataHash] from a [AuxiliaryData].
AuxiliaryDataHash.fromAuxiliaryData(AuxiliaryData data)
: super.fromBytes(
bytes: Hash.blake2b(
Uint8List.fromList(cbor.encode(data.toCbor())),
digestSize: _length,
),
);

@override
int get length => _length;
}
Loading

0 comments on commit 6d985bb

Please sign in to comment.