Skip to content
This repository was archived by the owner on Jun 1, 2026. It is now read-only.

Commit 30a514b

Browse files
committed
Integrate Cardano into signer
1 parent e7a8805 commit 30a514b

27 files changed

Lines changed: 1016 additions & 52 deletions

File tree

Cargo.lock

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/gem_algorand/src/models/signing/transaction.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ impl AlgorandTransaction {
2424
input,
2525
Operation::Payment {
2626
destination: AlgorandAddress::from_str(&input.destination_address).invalid_input("invalid Algorand address")?,
27-
amount: input.value.parse::<u64>().invalid_input("invalid Algorand amount")?,
27+
amount: input.value_as_u64()?,
2828
},
2929
)
3030
}
@@ -34,7 +34,7 @@ impl AlgorandTransaction {
3434
input,
3535
Operation::AssetTransfer {
3636
destination: AlgorandAddress::from_str(&input.destination_address).invalid_input("invalid Algorand address")?,
37-
amount: input.value.parse::<u64>().invalid_input("invalid Algorand amount")?,
37+
amount: input.value_as_u64()?,
3838
asset_id: get_asset_id(input)?,
3939
},
4040
)

crates/gem_cardano/Cargo.toml

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,52 @@ publish = false
66

77
[features]
88
default = []
9-
rpc = ["dep:chain_traits", "dep:gem_client"]
10-
reqwest = ["gem_client/reqwest"]
9+
rpc = [
10+
"dep:async-trait",
11+
"dep:bech32",
12+
"dep:chain_traits",
13+
"dep:chrono",
14+
"dep:futures",
15+
"dep:gem_client",
16+
"dep:hex",
17+
"dep:num-bigint",
18+
"dep:num-traits",
19+
"dep:serde_json",
20+
]
21+
signer = [
22+
"dep:bech32",
23+
"dep:curve25519-dalek",
24+
"dep:gem_hash",
25+
"dep:hex",
26+
"dep:num-traits",
27+
"dep:sha2",
28+
"dep:zeroize",
29+
]
30+
reqwest = ["dep:gem_client", "gem_client/reqwest"]
1131
chain_integration_tests = ["rpc", "reqwest", "settings/testkit"]
1232

1333
[dependencies]
1434
primitives = { path = "../primitives" }
35+
num-bigint = { workspace = true, optional = true }
36+
serde_serializers = { path = "../serde_serializers" }
37+
bech32 = { workspace = true, optional = true }
38+
gem_hash = { path = "../gem_hash", optional = true }
39+
futures = { workspace = true, optional = true }
40+
hex = { workspace = true, optional = true }
1541
serde = { workspace = true, features = ["derive"] }
16-
serde_json = { workspace = true }
17-
chrono = { workspace = true, features = ["serde"] }
18-
async-trait = { workspace = true }
42+
serde_json = { workspace = true, optional = true }
43+
chrono = { workspace = true, features = ["serde"], optional = true }
44+
async-trait = { workspace = true, optional = true }
1945
chain_traits = { path = "../chain_traits", optional = true }
2046
gem_client = { path = "../gem_client", optional = true }
21-
num-bigint = { workspace = true }
47+
num-traits = { workspace = true, optional = true }
48+
curve25519-dalek = { workspace = true, optional = true }
49+
sha2 = { workspace = true, optional = true }
50+
zeroize = { workspace = true, optional = true }
2251

2352
[dev-dependencies]
53+
gem_client = { path = "../gem_client", features = ["testkit"] }
54+
primitives = { path = "../primitives", features = ["testkit"] }
2455
tokio = { workspace = true, features = ["macros"] }
2556
reqwest = { workspace = true }
2657
settings = { path = "../settings", features = ["testkit"] }

crates/gem_cardano/src/address.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
use primitives::SignerError;
2+
3+
const MAINNET_NETWORK_ID: u8 = 1;
4+
const BASE_KEY_ADDRESS_TYPE: u8 = 0;
5+
const ENTERPRISE_KEY_ADDRESS_TYPE: u8 = 6;
6+
const ADDRESS_HEADER_LENGTH: usize = 1;
7+
const KEY_HASH_LENGTH: usize = 28;
8+
const ENTERPRISE_ADDRESS_LENGTH: usize = ADDRESS_HEADER_LENGTH + KEY_HASH_LENGTH;
9+
const BASE_ADDRESS_LENGTH: usize = ADDRESS_HEADER_LENGTH + KEY_HASH_LENGTH + KEY_HASH_LENGTH;
10+
11+
#[derive(Debug)]
12+
pub(crate) struct ShelleyAddress {
13+
bytes: Vec<u8>,
14+
}
15+
16+
impl ShelleyAddress {
17+
pub(crate) fn parse(address: &str) -> Result<Self, SignerError> {
18+
let (hrp, bytes) = bech32::decode(address).map_err(|_| SignerError::invalid_input("invalid Cardano address"))?;
19+
if hrp.as_str() != "addr" {
20+
return SignerError::invalid_input_err("unsupported Cardano address network");
21+
}
22+
23+
Self::from_bytes(bytes)
24+
}
25+
26+
pub(crate) fn as_bytes(&self) -> &[u8] {
27+
&self.bytes
28+
}
29+
30+
pub(crate) fn payment_hash(&self) -> &[u8] {
31+
&self.bytes[ADDRESS_HEADER_LENGTH..ADDRESS_HEADER_LENGTH + KEY_HASH_LENGTH]
32+
}
33+
34+
fn from_bytes(bytes: Vec<u8>) -> Result<Self, SignerError> {
35+
if bytes.is_empty() {
36+
return SignerError::invalid_input_err("invalid Cardano address");
37+
}
38+
let address = Self { bytes };
39+
if address.network_id() != MAINNET_NETWORK_ID {
40+
return SignerError::invalid_input_err("unsupported Cardano address network");
41+
}
42+
match address.address_type() {
43+
BASE_KEY_ADDRESS_TYPE => {
44+
if address.bytes.len() != BASE_ADDRESS_LENGTH {
45+
return SignerError::invalid_input_err("invalid Cardano base address");
46+
}
47+
}
48+
ENTERPRISE_KEY_ADDRESS_TYPE => {
49+
if address.bytes.len() != ENTERPRISE_ADDRESS_LENGTH {
50+
return SignerError::invalid_input_err("invalid Cardano enterprise address");
51+
}
52+
}
53+
_ => return SignerError::invalid_input_err("unsupported Cardano address type"),
54+
}
55+
Ok(address)
56+
}
57+
58+
fn address_type(&self) -> u8 {
59+
self.bytes[0] >> 4
60+
}
61+
62+
fn network_id(&self) -> u8 {
63+
self.bytes[0] & 0x0f
64+
}
65+
}
66+
67+
#[cfg(test)]
68+
mod tests {
69+
use super::*;
70+
71+
#[test]
72+
fn test_shelley_address_decode() {
73+
let address = ShelleyAddress::parse("addr1q8043m5heeaydnvtmmkyuhe6qv5havvhsf0d26q3jygsspxlyfpyk6yqkw0yhtyvtr0flekj84u64az82cufmqn65zdsylzk23").unwrap();
74+
assert_eq!(
75+
hex::encode(address.as_bytes()),
76+
"01df58ee97ce7a46cd8bdeec4e5f3a03297eb197825ed5681191110804df22424b6880b39e4bac8c58de9fe6d23d79aaf44756389d827aa09b"
77+
);
78+
assert_eq!(hex::encode(address.payment_hash()), "df58ee97ce7a46cd8bdeec4e5f3a03297eb197825ed5681191110804");
79+
}
80+
81+
#[test]
82+
fn test_shelley_address_rejects_unsupported() {
83+
assert!(ShelleyAddress::parse("stake1uykptcz226y5r5at5rfqqm00p9n0z0yfajz3gk3j3wm8dxg2sn0r4").is_err());
84+
assert!(ShelleyAddress::parse("addr_test1qr4p6f6mm0q9kfyyd9u30umk9cc6gk0nxu25k5rsc4fp7ls7k0qqxslcwwj4gvn4yfmdyrfgwjt3ztuz4zpy4242u0m95r0n").is_err());
85+
}
86+
}

crates/gem_cardano/src/cbor.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
pub(crate) struct CborEncoder {
2+
bytes: Vec<u8>,
3+
}
4+
5+
impl CborEncoder {
6+
pub(crate) fn new() -> Self {
7+
Self { bytes: Vec::new() }
8+
}
9+
10+
pub(crate) fn into_bytes(self) -> Vec<u8> {
11+
self.bytes
12+
}
13+
14+
pub(crate) fn raw(&mut self, raw: &[u8]) {
15+
self.bytes.extend_from_slice(raw);
16+
}
17+
18+
pub(crate) fn unsigned(&mut self, value: u64) {
19+
self.major_type(0, value);
20+
}
21+
22+
pub(crate) fn bytes(&mut self, value: &[u8]) {
23+
self.major_type(2, value.len() as u64);
24+
self.bytes.extend_from_slice(value);
25+
}
26+
27+
pub(crate) fn array(&mut self, len: usize) {
28+
self.major_type(4, len as u64);
29+
}
30+
31+
pub(crate) fn map(&mut self, len: usize) {
32+
self.major_type(5, len as u64);
33+
}
34+
35+
pub(crate) fn null(&mut self) {
36+
self.bytes.push(0xf6);
37+
}
38+
39+
pub(crate) fn true_value(&mut self) {
40+
self.bytes.push(0xf5);
41+
}
42+
43+
fn major_type(&mut self, major: u8, value: u64) {
44+
let prefix = major << 5;
45+
match value {
46+
0..=23 => self.bytes.push(prefix | value as u8),
47+
24..=0xff => {
48+
self.bytes.push(prefix | 24);
49+
self.bytes.push(value as u8);
50+
}
51+
0x100..=0xffff => {
52+
self.bytes.push(prefix | 25);
53+
self.bytes.extend_from_slice(&(value as u16).to_be_bytes());
54+
}
55+
0x1_0000..=0xffff_ffff => {
56+
self.bytes.push(prefix | 26);
57+
self.bytes.extend_from_slice(&(value as u32).to_be_bytes());
58+
}
59+
_ => {
60+
self.bytes.push(prefix | 27);
61+
self.bytes.extend_from_slice(&value.to_be_bytes());
62+
}
63+
}
64+
}
65+
}

crates/gem_cardano/src/lib.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
1+
#[cfg(any(feature = "rpc", feature = "signer"))]
2+
mod address;
3+
#[cfg(any(feature = "rpc", feature = "signer"))]
4+
mod cbor;
15
pub mod models;
6+
#[cfg(any(feature = "rpc", feature = "signer"))]
7+
mod planner;
8+
#[cfg(feature = "rpc")]
29
pub mod provider;
10+
#[cfg(feature = "rpc")]
311
pub mod rpc;
12+
#[cfg(any(feature = "rpc", feature = "signer"))]
13+
mod transaction;
414

15+
#[cfg(feature = "rpc")]
516
pub use provider::map_transaction;
17+
#[cfg(feature = "rpc")]
618
pub use rpc::client::CardanoClient;
19+
20+
#[cfg(feature = "signer")]
21+
pub mod signer;

crates/gem_cardano/src/models/block.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
use serde::{Deserialize, Serialize};
2+
use serde_serializers::deserialize_u64_from_str;
23

34
use super::UInt64;
45

56
#[derive(Debug, Clone, Serialize, Deserialize)]
67
#[serde(rename_all = "camelCase")]
78
pub struct Block {
89
pub number: UInt64,
10+
#[serde(deserialize_with = "deserialize_u64_from_str")]
11+
pub slot_no: UInt64,
912
}
1013

1114
#[derive(Debug, Clone, Serialize, Deserialize)]

0 commit comments

Comments
 (0)