Skip to content

Commit

Permalink
feat: add shelley address & cardano serialization lib
Browse files Browse the repository at this point in the history
  • Loading branch information
Dominik Toton committed May 9, 2024
1 parent 6e3c256 commit 6c76254
Show file tree
Hide file tree
Showing 10 changed files with 302 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .config/dictionaries/project.dic
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,7 @@ xcworkspace
yoroi
multiplatform
Multiplatform
Easterling
lovelace
lovelaces
pinenacl
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 @@
export 'src/address.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// 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.magicId == (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() {
return CborBytes(bytes);
}

@override
int get hashCode => bytes.hashCode;

@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() => bytes.toString();

// 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,57 @@
/// Exception thrown when the transaction exceeds the allowed maximum size.
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() {
return 'MaxTxSizeExceededException('
'maxTxSize=$maxTxSize'
', actualTxSize:$actualTxSize'
')';
}
}

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

@override
String toString() {
return 'TxFeeNotSpecifiedException';
}
}

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

@override
String toString() {
return 'TransactionHashFormatException';
}
}

/// Exception thrown if the address doesn't match the bech32 specification
/// for Shelley addresses.
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,21 @@
name: catalyst_cardano_serialization
description: Dart package providing serialization/deserialization for common structures for Cardano blockchain.
repository: https://github.com/input-output-hk/catalyst-voices/tree/main/catalyst_voices_packages/catalyst_cardano_serialization
issue_tracker: https://github.com/input-output-hk/catalyst-voices/issues
topics: [blockchain, cardano, cryptocurrency, wallet]
version: 0.1.0

environment:
sdk: ">=3.3.0 <4.0.0"

dependencies:
bech32: ^0.2.2
bip32_ed25519: ^0.5.0
cbor: ^6.2.0
convert: ^3.1.1
pinenacl: ^0.5.1

dev_dependencies:
catalyst_analysis:
path: ../catalyst_analysis
test: ^1.24.9
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import 'package:catalyst_cardano_serialization/src/address.dart';
import 'package:catalyst_cardano_serialization/src/types.dart';
import 'package:test/test.dart';

void main() {
group(ShelleyAddress, () {
final mainnetAddr = ShelleyAddress.fromBech32(
'addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqws'
'x5wktcd8cc3sq835lu7drv2xwl2wywfgse35a3x',
);
final testnetAddr = ShelleyAddress.fromBech32(
'addr_test1vz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspjrlsz',
);
final mainnetStakeAddr = ShelleyAddress.fromBech32(
'stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw',
);
final testnetStakeAddr = ShelleyAddress.fromBech32(
'stake_test1uqehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gssrtvn',
);

test('round-trip conversion from and to bytes', () {
expect(
ShelleyAddress(mainnetAddr.bytes, hrp: mainnetAddr.hrp),
equals(mainnetAddr),
);

expect(
ShelleyAddress(testnetAddr.bytes, hrp: testnetAddr.hrp),
equals(testnetAddr),
);

expect(
ShelleyAddress(mainnetStakeAddr.bytes, hrp: mainnetStakeAddr.hrp),
equals(mainnetStakeAddr),
);

expect(
ShelleyAddress(testnetStakeAddr.bytes, hrp: testnetStakeAddr.hrp),
equals(testnetStakeAddr),
);
});

test('round-trip conversion from and to bech32', () {
expect(
ShelleyAddress.fromBech32(mainnetAddr.toBech32()),
equals(mainnetAddr),
);

expect(
ShelleyAddress.fromBech32(testnetAddr.toBech32()),
equals(testnetAddr),
);

expect(
ShelleyAddress.fromBech32(mainnetStakeAddr.toBech32()),
equals(mainnetStakeAddr),
);

expect(
ShelleyAddress.fromBech32(testnetStakeAddr.toBech32()),
equals(testnetStakeAddr),
);
});

test('hrp from address', () {
expect(mainnetAddr.hrp, equals('addr'));
expect(testnetAddr.hrp, equals('addr_test'));
expect(mainnetStakeAddr.hrp, equals('stake'));
expect(testnetStakeAddr.hrp, equals('stake_test'));
});

test('network from address', () {
expect(mainnetAddr.network, equals(NetworkId.mainnet));
expect(testnetAddr.network, equals(NetworkId.testnet));
expect(mainnetStakeAddr.network, equals(NetworkId.mainnet));
expect(testnetStakeAddr.network, equals(NetworkId.testnet));
});
});
}

0 comments on commit 6c76254

Please sign in to comment.