Skip to content

Commit

Permalink
Merge branch 'master' into s/thorchain-chain-id
Browse files Browse the repository at this point in the history
  • Loading branch information
satoshiotomakan committed Dec 13, 2024
2 parents bad9fcb + dd612bb commit 1f8d4fe
Show file tree
Hide file tree
Showing 16 changed files with 286 additions and 49 deletions.
8 changes: 4 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ let package = Package(
targets: [
.binaryTarget(
name: "WalletCore",
url: "https://github.com/trustwallet/wallet-core/releases/download/4.1.12/WalletCore.xcframework.zip",
checksum: "1632bbbab1c6a588689eec77a24e1468d9a6746968652cf0a7e669e996c3d24d"
url: "https://github.com/trustwallet/wallet-core/releases/download/4.1.21/WalletCore.xcframework.zip",
checksum: "503937e1013bb7c1f610a8a4ec74a6ccdedb06bdec9fa9126ac47e25a90da06a"
),
.binaryTarget(
name: "SwiftProtobuf",
url: "https://github.com/trustwallet/wallet-core/releases/download/4.1.12/SwiftProtobuf.xcframework.zip",
checksum: "33d80c20428c9db4fcf99d1272ba19655f7c6ee7e5b1809fa8a7e7d4aa1b222b"
url: "https://github.com/trustwallet/wallet-core/releases/download/4.1.21/SwiftProtobuf.xcframework.zip",
checksum: "f6da2b8fafdce5e8d46ea305972f1ad942cc796f63026a32c883331dd3813285"
)
]
)
2 changes: 1 addition & 1 deletion docs/registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ This list is generated from [./registry.json](../registry.json)
| 10004689 | IoTeX EVM | IOTX | <img src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/iotexevm/info/logo.png" width="32" /> | <https://iotex.io/> |
| 10007000 | NativeZetaChain | ZETA | <img src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/zetachain/info/logo.png" width="32" /> | <https://www.zetachain.com/> |
| 10007700 | NativeCanto | CANTO | <img src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/nativecanto/info/logo.png" width="32" /> | <https://canto.io/> |
| 10008217 | Kaia | KLAY | <img src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/kaia/info/logo.png" width="32" /> | <https://kaia.io> |
| 10008217 | Kaia | KAIA | <img src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/kaia/info/logo.png" width="32" /> | <https://kaia.io> |
| 10009000 | Avalanche C-Chain | AVAX | <img src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/avalanchec/info/logo.png" width="32" /> | <https://www.avalabs.org/> |
| 10009001 | Evmos | EVMOS | <img src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/evmos/info/logo.png" width="32" /> | <https://evmos.org/> |
| 10042170 | Arbitrum Nova | ETH | <img src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/arbitrumnova/info/logo.png" width="32" /> | <https://nova.arbitrum.io> |
Expand Down
2 changes: 1 addition & 1 deletion registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -4066,7 +4066,7 @@
"id": "kaia",
"name": "Kaia",
"coinId": 10008217,
"symbol": "KLAY",
"symbol": "KAIA",
"decimals": 18,
"blockchain": "Ethereum",
"derivation": [
Expand Down
7 changes: 0 additions & 7 deletions rust/chains/tw_solana/src/modules/tx_signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,6 @@ impl TxSigner {
) -> SigningResult<versioned::VersionedTransaction> {
let mut tx = versioned::VersionedTransaction::unsigned(unsigned_msg);

let actual_signatures = key_signs.len();
let expected_signatures = tx.message.num_required_signatures();
if actual_signatures != expected_signatures {
return SigningError::err(SigningErrorType::Error_signatures_count)
.with_context(|| format!("Expected '{expected_signatures}' signatures, provided '{actual_signatures}'"));
}

for (signing_pubkey, ed25519_signature) in key_signs {
// Find an index of the corresponding account.
let account_index = tx
Expand Down
14 changes: 8 additions & 6 deletions rust/frameworks/tw_utxo/src/modules/sighash_computer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ where
.input_args()
.iter()
.enumerate()
.map(|(input_index, utxo)| {
.map(|(signing_input_index, utxo)| {
let signing_method = utxo.signing_method;

let utxo_args = UtxoPreimageArgs {
input_index,
input_index: signing_input_index,
script_pubkey: utxo.script_pubkey.clone(),
amount: utxo.amount,
// TODO move `leaf_hash_code_separator` to `UtxoTaprootPreimageArgs`.
Expand All @@ -90,12 +90,14 @@ where
let tr_spent_script_pubkeys: Vec<Script> = unsigned_tx
.input_args()
.iter()
.map(|utxo| {
if utxo.signing_method == SigningMethod::Taproot {
// Taproot UTXOs scriptPubkeys should be signed as is.
.enumerate()
.map(|(i, utxo)| {
if i == signing_input_index {
// Use the scriptPubkey required to spend this UTXO.
utxo.script_pubkey.clone()
} else {
// Use the original scriptPubkey declared in the unspent output.
// Use the original scriptPubkey declared in the unspent output for other UTXOs
// (different from that we sign at this iteration).
utxo.prevout_script_pubkey.clone()
}
})
Expand Down
2 changes: 1 addition & 1 deletion rust/tw_keypair/src/ed25519/keypair.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ impl<H: Hasher512> SigningKeyTrait for KeyPair<H> {
type Signature = Signature;

fn sign(&self, message: Self::SigningMessage) -> KeyPairResult<Self::Signature> {
self.private().sign_with_public_key(self.public(), &message)
self.private().sign(message)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ impl<H: Hasher512> SigningKeyTrait for ExtendedKeyPair<H> {
type Signature = Signature;

fn sign(&self, message: Self::SigningMessage) -> KeyPairResult<Self::Signature> {
self.private()
.sign_with_public_key(self.public(), message.as_slice())
self.private().sign(message)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,25 +43,16 @@ impl<H: Hasher512> ExtendedPrivateKey<H> {

ExtendedPublicKey::new(key, second_key)
}

/// `ed25519` signing uses a public key associated with the private key.
pub(crate) fn sign_with_public_key(
&self,
public: &ExtendedPublicKey<H>,
message: &[u8],
) -> KeyPairResult<Signature> {
self.key
.expanded_key
.sign_with_pubkey(public.key_for_signing(), message)
}
}

impl<H: Hasher512> SigningKeyTrait for ExtendedPrivateKey<H> {
type SigningMessage = Vec<u8>;
type Signature = Signature;

fn sign(&self, message: Self::SigningMessage) -> KeyPairResult<Self::Signature> {
self.sign_with_public_key(&self.public(), message.as_slice())
self.key
.expanded_key
.dangerous_sign_with_pubkey(self.public().key_for_signing(), message.as_slice())
}
}

Expand Down
13 changes: 2 additions & 11 deletions rust/tw_keypair/src/ed25519/private.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,24 +36,15 @@ impl<H: Hasher512> PrivateKey<H> {
pub fn public(&self) -> PublicKey<H> {
PublicKey::with_expanded_secret(&self.expanded_key)
}

/// `ed25519` signing uses a public key associated with the private key.
pub(crate) fn sign_with_public_key(
&self,
public: &PublicKey<H>,
message: &[u8],
) -> KeyPairResult<Signature> {
self.expanded_key
.sign_with_pubkey(public.to_bytes(), message)
}
}

impl<H: Hasher512> SigningKeyTrait for PrivateKey<H> {
type SigningMessage = Vec<u8>;
type Signature = Signature;

fn sign(&self, message: Self::SigningMessage) -> KeyPairResult<Self::Signature> {
self.sign_with_public_key(&self.public(), &message)
self.expanded_key
.dangerous_sign_with_pubkey(self.public().to_bytes(), &message)
}
}

Expand Down
12 changes: 10 additions & 2 deletions rust/tw_keypair/src/ed25519/secret.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,14 @@ impl<H: Hasher512> ExpandedSecretKey<H> {
/// Signs a message with this `ExpandedSecretKey`.
/// Source: https://github.com/dalek-cryptography/ed25519-dalek/blob/1.0.1/src/secret.rs#L389-L412
/// Ported: https://github.com/trustwallet/wallet-core/blob/423f0e34725f69c0a9d535e1a32534c99682edea/trezor-crypto/crypto/ed25519-donna/ed25519.c#L97-L130
///
/// # Important
///
/// Ensure that the public key is always correctly paired with the private key,
/// preventing scenarios where an arbitrary public key could be introduced into the signing process.
/// Security report: https://github.com/trustwallet/wallet-core/security/advisories/GHSA-7g72-jxww-q9vq
#[allow(non_snake_case)]
pub(crate) fn sign_with_pubkey(
pub(crate) fn dangerous_sign_with_pubkey(
&self,
pubkey: H256,
message: &[u8],
Expand Down Expand Up @@ -122,7 +128,9 @@ mod tests {
let message = hex::decode("f0").unwrap();

// Anyway, the result signature has an expected value.
let sign = secret_key.sign_with_pubkey(public, &message).unwrap();
let sign = secret_key
.dangerous_sign_with_pubkey(public, &message)
.unwrap();
let expected = H512::from("ed55bce14a845a275e7a3a7242420ed1eeaba79dc3141bebf42ca0d12169e209a6e56b6981a336f711ae3aaea8d063b72b0e79a8808311d08cb42cabfdd0450d");
assert_eq!(sign.to_bytes(), expected);
}
Expand Down
74 changes: 74 additions & 0 deletions rust/tw_tests/tests/chains/bitcoin/bitcoin_sign/brc20.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,77 @@ fn test_bitcoin_sign_brc20_transfer() {
fee: 3000,
});
}

/// Fixes `{"error":"-26: non-mandatory-script-verify-flag (Invalid Schnorr signature)"}` error.
#[test]
fn test_bitcoin_sign_brc20_reveal_with_extra_p2tr_input() {
// bc1puq428nh4eynlqph8gynwdtqg4je0hc03gp2ptgsf4c5ylxz0ll2sd34gk7
let alice_pk_bytes = "8efa479919269076eb331c304fff187b9d7aa60d1f6cd3d6b12a151a52f22582"
.decode_hex()
.unwrap();
let alice_private_key = schnorr::PrivateKey::try_from(alice_pk_bytes.as_slice()).unwrap();
let alice_pubkey = alice_private_key.public().compressed();
let my_address = "bc1puq428nh4eynlqph8gynwdtqg4je0hc03gp2ptgsf4c5ylxz0ll2sd34gk7";

let commit_txid = "164b459a49c5e3a817028df7f4545585874feff3985e48d6ff6792989a4823a8";
let commit_utxo = Proto::Input {
out_point: input::out_point(commit_txid, 0),
value: 546,
sighash_type: SIGHASH_ALL,
claiming_script: input::brc20_inscribe(alice_pubkey.to_vec(), "duna", "0.001"),
..Default::default()
};

// Extra P2TR UTXO is used to cover transaction fee.
let p2tr_utxo = "164b459a49c5e3a817028df7f4545585874feff3985e48d6ff6792989a4823a8";
let extra_p2tr = Proto::Input {
out_point: input::out_point(p2tr_utxo, 1),
value: 11_210,
sighash_type: SIGHASH_ALL,
claiming_script: input::receiver_address(my_address),
..Default::default()
};

let out1 = Proto::Output {
value: 546,
to_recipient: output::to_address(my_address),
};
let change_output = Proto::Output {
value: 0,
to_recipient: output::to_address(my_address),
};

let builder = Proto::TransactionBuilder {
version: Proto::TransactionVersion::V2,
inputs: vec![commit_utxo, extra_p2tr],
outputs: vec![out1],
change_output: Some(change_output),
input_selector: Proto::InputSelector::UseAll,
dust_policy: dust_threshold(DUST),
fee_per_vb: 9,
..Default::default()
};

let signing = Proto::SigningInput {
private_keys: vec![alice_pk_bytes.into()],
chain_info: btc_info(),
// We enable deterministic Schnorr signatures here
dangerous_use_fixed_schnorr_rng: true,
transaction: TransactionOneof::builder(builder),
..Default::default()
};

// https://www.blockchain.com/explorer/transactions/btc/113dfc827e4535dccc6aa7fcff5482b4de0fb2ab70f52c44c12c12bca3be5847
sign::BitcoinSignHelper::new(&signing)
.coin(CoinType::Bitcoin)
.sign(sign::Expected {
encoded: "02000000000102a823489a989267ffd6485e98f3ef4f87855554f4f78d0217a8e3c5499a454b160000000000ffffffffa823489a989267ffd6485e98f3ef4f87855554f4f78d0217a8e3c5499a454b160100000000ffffffff022202000000000000225120e02aa3cef5c927f006e74126e6ac08acb2fbe1f1405415a209ae284f984fffd52d23000000000000225120e02aa3cef5c927f006e74126e6ac08acb2fbe1f1405415a209ae284f984fffd50340b90b099a8facd5d4e6008990d180f9afeeb07d62452570a6e700ca0f7968577da6113cb201e9ac3584caa04898cdc7113cce4df06604b8f294a3959d216d364f5e0063036f7264010118746578742f706c61696e3b636861727365743d7574662d38003a7b2270223a226272632d3230222c226f70223a227472616e73666572222c227469636b223a2264756e61222c22616d74223a22302e303031227d6821c02146f58256fcc00ef86a0e53fc14e943bbea2c7972b598b58178fdd6fa3ef79201401c5e54a0ead877e52146e42f8d197f4c7be84c7d2479a75f33128f15777584bfec410c3e130b4504fe061991a78365add223a3dfb5ca79a988f9ffcf46039b3100000000",
txid: "113dfc827e4535dccc6aa7fcff5482b4de0fb2ab70f52c44c12c12bca3be5847",
inputs: vec![546, 11_210],
outputs: vec![546, 9_005],
// `vsize` is different from the estimated value due to the signatures der serialization.
vsize: 244,
weight: 975,
fee: 2_205,
});
}
53 changes: 53 additions & 0 deletions rust/tw_tests/tests/chains/solana/solana_compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
// Copyright © 2017 Trust Wallet.

use tw_any_coin::test_utils::sign_utils::{CompilerHelper, PreImageHelper};
use tw_any_coin::test_utils::transaction_decode_utils::TransactionDecoderHelper;
use tw_coin_registry::coin_type::CoinType;
use tw_encoding::base58::{self, Alphabet};
use tw_encoding::base64::{self, STANDARD};
use tw_encoding::hex::{DecodeHex, ToHex};
use tw_proto::Common::Proto::SigningError;
use tw_proto::Solana::Proto::{self, mod_SigningInput::OneOftransaction_type as TransactionType};
Expand Down Expand Up @@ -421,3 +423,54 @@ fn test_solana_compile_transfer_with_fake_signature() {
let output = compiler.compile(CoinType::Solana, &input, vec![signature], vec![public_key]);
assert_eq!(output.error, SigningError::Error_signing);
}

#[test]
fn test_solana_compile_with_partial_signature() {
// The following is an unsigned transaction generated by the Jito Staking DApp.
// This transaction requires two signatures; only the first one will be provided by the user.
// The DApp will add the second signature before submitting the transaction to Solana network.
let encoded_unsigned_tx = base64::decode("AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAGDqMrdVJnGyKZfDMMU6yR6mjQRynQy91x5Ik0QW5S7HjblEDFOjfYkcjeiZdpl9opTtGz1XgRDjyGqc4Z5VKuBHI5lg1U5Wnl5tFnrGEI9Y2rG1BNC7tYK5PXpURkpupeap6naP7fZEyKrpuOIYit0GvFUPv3Fsgiuc5jx3g9lS4fsU4N5V6fuoY5br/VSM/4ySAR6se3W6qbLZxqhvWhcUEJ5qP+7PmQMuHB32uXItyzY057jjRAk2vDSwzByOtSH/zRQemDLK8QrZF0lcoPJxtbKTzUcCfqc3AH7UDrOaC9BIo+CMO0lb4X9FQn2JvsW4DH4mlcGGTXZ0PbOb7TRtYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFTlniTAUaihSnsel5fw4Szfp0kCFmUxlaalEqbxZmArjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+FkDBkZv5SEXMv/srbpyw5vnvIzlu8X3EmssQ5s6QAAAAAaBTtTK9ooXRnL9rIYDGmPoTqFe+h1EtyKT9tvbABZQBt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKnLHFG8r27UznARMamsTdStWltOfL+TLpgyxe4Hfqm8xQYIAgABDAIAAACghgEAAAAAAAoGAAIABggNAQEMCgcJAwECBQIGCA0JDqCGAQAAAAAACwAFAkANAwALAAkDgDgBAAAAAAAIAgAEDAIAAAC0/gAAAAAAAA==", STANDARD).unwrap();

// Step 1: Decode the transaction, get the raw message, and prepare the signing input
let mut decoder = TransactionDecoderHelper::<Proto::DecodingTransactionOutput>::default();
let output = decoder.decode(CoinType::Solana, encoded_unsigned_tx);
assert_eq!(output.error, SigningError::OK);

let input = Proto::SigningInput {
raw_message: output.transaction,
..Proto::SigningInput::default()
};

// Step 2: Simulate signature (the first signature), normally obtained from signature server.
let signature1 = "daf56b31e3a97504bb5adb4c64709cdd8240700935d9e1cae9a05ab032de04b469cc1e02587f9c6d7e57192e4f9c77117897a85d15900730d90a7f55debf690a".decode_hex().unwrap();
let public_key1 = "a32b7552671b22997c330c53ac91ea68d04729d0cbdd71e48934416e52ec78db"
.decode_hex()
.unwrap();

// Step 3: Compile the transaction with partial signature
let mut compiler = CompilerHelper::<Proto::SigningOutput>::default();
let output = compiler.compile(
CoinType::Solana,
&input,
vec![signature1],
vec![public_key1],
);
assert_eq!(output.error, SigningError::OK);
assert_eq!(output.encoded, "YziLZ5ChTunpGLAAufaXrSpPrFZsL9fsiyo2NshjFMUXbyRrFbpTZpLdsWWULCvKeg4oSdxDuaDqazKZjNgdFjh7bDzq7HPzafzGRHEWy3Rub9DK8uUJrZe6EHpZSm1EKLsbFXYHaoRPPpuT7Pywufjdsk79qWGMHba6KnSaXbRm1tY2Fz88Hz25GUKvvKg7aSUJ5CZ3E2EEcWuX1VBDXPdzcgFkWtrW7JmjfNfhcxKQ5rHet8h3Cr1DueXoz12Sso2gKMgEjAdeHsMFpACercWtWW8M6B8VKp4KqJDvDUCRcJU9U6EbfiTt25dZEuDg4S7ceJG8nHJrYVEKajkPr5HZeU5zuxKQix1xUKBTz7Dx3U4ddFMSSb4xYb7Hv5oacUb3FRP7HwFYkuRocME8fr4wCxGnXg4Tkq1Fe9sVK1nD4YEiD77m6KD44bbXubqQJqkiLuXRJB28Jc3MUPJHrWRJhS5ezL1y89sL91cqZ9fpnLvih4MDC3VcCeuV35LxbigwHvcPPVxsHo2DdKraYMEf5kpR8eqbftAvQbpcUbxeraPcvhPKqU1GtkAgYCL6XG3Y44io5eTQwqMvLwRW9SsFhmbGCk1i7vBPf4p6EmVs3zbfKKSBXK7tg8RD4ojJiGBqCr3yuZkNeFoFXS1JhkggoS8Ury9xLrAB2mmCHEnyr9x5jEspyCcFUtxSyvtCGiyQVG9HxMEzJKVLX27vYncy1URzGyUS9TF35mpXRo3kUNVjTNuMXmYyMSGunt84atiCWSUMPWRB6sjJD9YBv35pLok589m5WvYvB1XFH9sSfkDQ8RCMpu762ekSJSQev3aH1F61hucKSkXNrkCvW8J2wLrLvwSjoPZ28u74AcvpzEa8o3ks33EYdNWTdY1aWWFzqRbCyQPE2s6JmRDJgX57QYdMqXs1KDkhgb7ttQrrkfgb5ki5EofcmNw");

// Step 4: Return the compiled transaction with partial signature to the DApp, which will then complete the remaining signatures.
let mut tx_with_partial_signature =
base58::decode(output.encoded.as_ref(), Alphabet::Bitcoin).unwrap();
// The following is the signature (the second signature) generated by the DApp.
let signature_generated_by_dapp = "c3207d277754a1a872bdc910136e917f9e902f025e493f0c1bf3d5b4512fffe03b250e86e4e68e20e96ab09beb24d7706ae338a2c5d11fbb23d779ae5b8c0701".decode_hex().unwrap();
// The following is a simple code to simulate the process of the DApp filling in the second signature.
let second_signature_offset = 1 + 64;
let second_signature_length = 64;
tx_with_partial_signature
[second_signature_offset..(second_signature_offset + second_signature_length)]
.copy_from_slice(&signature_generated_by_dapp);

let full_signed_tx = base64::encode(&tx_with_partial_signature, STANDARD);
// Successfully broadcasted: https://solscan.io/tx/5NuXtYpE58FbtCfEzgk2cTHZgEdNF69Z76bd8TQhBgmEN1RC98DNGNiWhvp1VSDMPudCgpE3z8jD7BNRuBztUbpM
assert_eq!(full_signed_tx, "Atr1azHjqXUEu1rbTGRwnN2CQHAJNdnhyumgWrAy3gS0acweAlh/nG1+VxkuT5x3EXiXqF0VkAcw2Qp/Vd6/aQrDIH0nd1ShqHK9yRATbpF/npAvAl5JPwwb89W0US//4DslDobk5o4g6Wqwm+sk13Bq4ziixdEfuyPXea5bjAcBAgAGDqMrdVJnGyKZfDMMU6yR6mjQRynQy91x5Ik0QW5S7HjblEDFOjfYkcjeiZdpl9opTtGz1XgRDjyGqc4Z5VKuBHI5lg1U5Wnl5tFnrGEI9Y2rG1BNC7tYK5PXpURkpupeap6naP7fZEyKrpuOIYit0GvFUPv3Fsgiuc5jx3g9lS4fsU4N5V6fuoY5br/VSM/4ySAR6se3W6qbLZxqhvWhcUEJ5qP+7PmQMuHB32uXItyzY057jjRAk2vDSwzByOtSH/zRQemDLK8QrZF0lcoPJxtbKTzUcCfqc3AH7UDrOaC9BIo+CMO0lb4X9FQn2JvsW4DH4mlcGGTXZ0PbOb7TRtYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFTlniTAUaihSnsel5fw4Szfp0kCFmUxlaalEqbxZmArjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+FkDBkZv5SEXMv/srbpyw5vnvIzlu8X3EmssQ5s6QAAAAAaBTtTK9ooXRnL9rIYDGmPoTqFe+h1EtyKT9tvbABZQBt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKnLHFG8r27UznARMamsTdStWltOfL+TLpgyxe4Hfqm8xQYIAgABDAIAAACghgEAAAAAAAoGAAIABggNAQEMCgcJAwECBQIGCA0JDqCGAQAAAAAACwAFAkANAwALAAkDgDgBAAAAAAAIAgAEDAIAAAC0/gAAAAAAAA==")
}
Loading

0 comments on commit 1f8d4fe

Please sign in to comment.