Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/ptr-addr-u64' into evgenii/slots…
Browse files Browse the repository at this point in the history
…-bigint
  • Loading branch information
vsubhuman committed May 14, 2022
2 parents bdd2c7e + 2158fe8 commit 3ffa5fa
Show file tree
Hide file tree
Showing 2 changed files with 313 additions and 20 deletions.
251 changes: 251 additions & 0 deletions doc/getting-started/minting-nfts.md
@@ -0,0 +1,251 @@

# Minting Nfts using yoroi backend and cardano-serialization-lib

This example mints nfts directly to an account on testnet.

code taken from (here)[https://github.com/ozgrakkurt/cardano-mint-nft]

```javascript
import CardanoWasm from "@emurgo/cardano-serialization-lib-nodejs";
import axios from "axios";
import cbor from "cbor";

const mintNft = async (
privateKey,
policy,
assetName,
description,
imageUrl,
mediaType
) => {
const FEE = 300000;

const publicKey = privateKey.to_public();

const addr = CardanoWasm.BaseAddress.new(
CardanoWasm.NetworkInfo.testnet().network_id(),
CardanoWasm.StakeCredential.from_keyhash(publicKey.hash()),
CardanoWasm.StakeCredential.from_keyhash(publicKey.hash())
).to_address();

const policyPubKey = policy.privateKey.to_public();

const policyAddr = CardanoWasm.BaseAddress.new(
CardanoWasm.NetworkInfo.testnet().network_id(),
CardanoWasm.StakeCredential.from_keyhash(policyPubKey.hash()),
CardanoWasm.StakeCredential.from_keyhash(policyPubKey.hash())
).to_address();

console.log(`ADDR: ${addr.to_bech32()}`);

// get utxos for our address and select one that is probably big enough to pay the tx fee
const utxoRes = await axios.post(
"https://testnet-backend.yoroiwallet.com/api/txs/utxoForAddresses",
{
addresses: [addr.to_bech32()],
}
);

let utxo = null;

if (utxoRes.data) {
for (const utxoEntry of utxoRes.data) {
if (utxoEntry.amount > FEE) {
utxo = utxoEntry;
}
}
}

if (utxo === null) {
throw new Error("no utxo found with sufficient ADA.");
}

console.log(`UTXO: ${JSON.stringify(utxo, null, 4)}`);

// get current global slot from yoroi backend
const { data: slotData } = await axios.get(
"https://testnet-backend.yoroiwallet.com/api/v2/bestblock"
);

const ttl = slotData.globalSlot + 60 * 60 * 2; // two hours from now

const txBuilder = CardanoWasm.TransactionBuilder.new(
CardanoWasm.TransactionBuilderConfigBuilder.new()
.fee_algo(
CardanoWasm.LinearFee.new(
CardanoWasm.BigNum.from_str("44"),
CardanoWasm.BigNum.from_str("155381")
)
)
.coins_per_utxo_word(CardanoWasm.BigNum.from_str("34482"))
.pool_deposit(CardanoWasm.BigNum.from_str("500000000"))
.key_deposit(CardanoWasm.BigNum.from_str("2000000"))
.max_value_size(5000)
.max_tx_size(16384)
.build()
);

const scripts = CardanoWasm.NativeScripts.new();

const policyKeyHash = CardanoWasm.BaseAddress.from_address(policyAddr)
.payment_cred()
.to_keyhash();

console.log(
`POLICY_KEYHASH: ${Buffer.from(policyKeyHash.to_bytes()).toString("hex")}`
);

// add key hash script so only people with policy key can mint assets using this policyId
const keyHashScript = CardanoWasm.NativeScript.new_script_pubkey(
CardanoWasm.ScriptPubkey.new(policyKeyHash)
);
scripts.add(keyHashScript);

const policyTtl = policy.ttl || ttl;

console.log(`POLICY_TTL: ${policyTtl}`);

// add timelock so policy is locked after this slot
const timelock = CardanoWasm.TimelockExpiry.new(policyTtl);
const timelockScript = CardanoWasm.NativeScript.new_timelock_expiry(timelock);
scripts.add(timelockScript);

const mintScript = CardanoWasm.NativeScript.new_script_all(
CardanoWasm.ScriptAll.new(scripts)
);

const privKeyHash = CardanoWasm.BaseAddress.from_address(addr)
.payment_cred()
.to_keyhash();
txBuilder.add_key_input(
privKeyHash,
CardanoWasm.TransactionInput.new(
CardanoWasm.TransactionHash.from_bytes(Buffer.from(utxo.tx_hash, "hex")),
utxo.tx_index
),
CardanoWasm.Value.new(CardanoWasm.BigNum.from_str(utxo.amount))
);

txBuilder.add_mint_asset_and_output_min_required_coin(
mintScript,
CardanoWasm.AssetName.new(Buffer.from(assetName)),
CardanoWasm.Int.new_i32(1),
CardanoWasm.TransactionOutputBuilder.new().with_address(addr).next()
);

const policyId = Buffer.from(mintScript.hash().to_bytes()).toString("hex");

console.log(`POLICY_ID: ${policyId}`);

const metadata = {
[policyId]: {
[assetName]: {
name: assetName,
description,
image: imageUrl,
mediaType,
},
},
};

console.log(`METADATA: ${JSON.stringify(metadata, null, 4)}`);

// transaction ttl can't be later than policy ttl
const txTtl = ttl > policyTtl ? policyTtl : ttl;

console.log(`TX_TTL: ${txTtl}`);

txBuilder.set_ttl(txTtl);
txBuilder.add_json_metadatum(
CardanoWasm.BigNum.from_str("721"),
JSON.stringify(metadata)
);

txBuilder.add_change_if_needed(addr);

const txBody = txBuilder.build();
const txHash = CardanoWasm.hash_transaction(txBody);

console.log(`TX_HASH: ${Buffer.from(txHash.to_bytes()).toString("hex")}`);

// sign the tx using the policy key and main key
const witnesses = CardanoWasm.TransactionWitnessSet.new();
const vkeyWitnesses = CardanoWasm.Vkeywitnesses.new();
vkeyWitnesses.add(CardanoWasm.make_vkey_witness(txHash, policy.privateKey));
vkeyWitnesses.add(CardanoWasm.make_vkey_witness(txHash, privateKey));
witnesses.set_vkeys(vkeyWitnesses);
witnesses.set_native_scripts;
const witnessScripts = CardanoWasm.NativeScripts.new();
witnessScripts.add(mintScript);
witnesses.set_native_scripts(witnessScripts);

const unsignedTx = txBuilder.build_tx();

// create signed transaction
const tx = CardanoWasm.Transaction.new(
unsignedTx.body(),
witnesses,
unsignedTx.auxiliary_data()
);

const signedTx = Buffer.from(tx.to_bytes()).toString("base64");

// submit the transaction using yoroi backend
try {
const { data } = await axios.post(
"https://testnet-backend.yoroiwallet.com/api/txs/signed",
{
signedTx,
}
);

console.log(`SUBMIT_RESULT: ${JSON.stringify(data, null, 4)}`);
} catch (error) {
console.error(
`failed to submit tx via yoroi backend: ${error.toString()}. error details: ${JSON.stringify(
error.response?.data
)}`
);
}
};
try {
const privateKey = CardanoWasm.PrivateKey.from_bech32(
//"ed25519_sk1fde2u8u2qme8uau5ac3w6c082gvtnmxt6uke2w8e07xwzewxee3q3n0f8e"
"ed25519_sk18j0a6704zyerm6dsj6p2fp8juw5m43rfgk0y84jnm7w5khs4dpqquewh43"
);
console.log(`PRIVATE KEY: ${privateKey.to_bech32()}`);
/*
const policyPrivateKey = CardanoWasm.PrivateKey.from_bech32(
"ed25519_sk1q96x2g66j5g7u5wydl7kcagk0h8upxznt3gj48h6njqthkyr7faqxmnnte"
);
*/
// import policy key from a .skey file
const policyPrivateKey = CardanoWasm.PrivateKey.from_normal_bytes(
cbor.decodeFirstSync(
"582009ca7f508dd5a5f9823d367e98170f25606799f49ae7363a47a11d7d3502c91f"
)
);
console.log(`POLICY_PRIV_KEY: ${policyPrivateKey.to_bech32()}`);
await mintNft(
privateKey, // main key
{
privateKey: policyPrivateKey, // policy key
// pass null here to get automatic ttl for policy
// and paste the POLICY_TTL output you get in console to here to mint with same policy
ttl: null, // policy ttl
},
"asdNFT5", // assetName
"some descr this is a new nft with same policy", // description
"ipfs://QmNhmDPJMgdsFRM9HyiQEJqrKkpsWFshqES8mPaiFRq9Zk", // image url
"image/jpeg" // mediaType
);
} catch (err) {
console.error(`failed to mint nft: ${err.toString()}`);
}
```
82 changes: 62 additions & 20 deletions rust/src/utils.rs
Expand Up @@ -498,6 +498,8 @@ impl Deserialize for Value {
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Int(pub (crate) i128);

to_from_bytes!(Int);

#[wasm_bindgen]
impl Int {
pub fn new(x: &BigNum) -> Self {
Expand Down Expand Up @@ -592,13 +594,30 @@ impl Deserialize for Int {
(|| -> Result<_, DeserializeError> {
match raw.cbor_type()? {
cbor_event::Type::UnsignedInteger => Ok(Self(raw.unsigned_integer()? as i128)),
cbor_event::Type::NegativeInteger => Ok(Self(raw.negative_integer()? as i128)),
cbor_event::Type::NegativeInteger => Ok(Self(read_nint(raw)?)),
_ => Err(DeserializeFailure::NoVariantMatched.into()),
}
})().map_err(|e| e.annotate("Int"))
}
}

/// TODO: this function can be removed in case `cbor_event` library ever gets a fix on their side
/// See https://github.com/Emurgo/cardano-serialization-lib/pull/392
fn read_nint<R: BufRead + Seek>(raw: &mut Deserializer<R>) -> Result <i128, DeserializeError> {
let found = raw.cbor_type()?;
if found != cbor_event::Type::NegativeInteger {
return Err(cbor_event::Error::Expected(cbor_event::Type::NegativeInteger, found).into());
}
let (len, len_sz) = raw.cbor_len()?;
match len {
cbor_event::Len::Indefinite => Err(cbor_event::Error::IndefiniteLenNotSupported(cbor_event::Type::NegativeInteger).into()),
cbor_event::Len::Len(v) => {
raw.advance(1 + len_sz)?;
Ok(-(v as i128) - 1)
}
}
}

const BOUNDED_BYTES_CHUNK_SIZE: usize = 64;

pub (crate) fn write_bounded_bytes<'se, W: Write>(serializer: &'se mut Serializer<W>, bytes: &[u8]) -> cbor_event::Result<&'se mut Serializer<W>> {
Expand Down Expand Up @@ -725,22 +744,28 @@ impl cbor_event::se::Serialize for BigInt {
num_bigint::Sign::Minus => serializer.write_negative_integer(-(*u64_digits.first().unwrap() as i128) as i64),
},
_ => {
// Small edge case: nint's minimum is -18446744073709551616 but in this bigint lib
// that takes 2 u64 bytes so we put that as a special case here:
if sign == num_bigint::Sign::Minus && u64_digits == vec![0, 1] {
serializer.write_negative_integer(-18446744073709551616i128 as i64)
} else {
let (sign, bytes) = self.0.to_bytes_be();
match sign {
// positive bigint
num_bigint::Sign::Plus |
num_bigint::Sign::NoSign => {
serializer.write_tag(2u64)?;
write_bounded_bytes(serializer, &bytes)
},
// negative bigint
num_bigint::Sign::Minus => {
serializer.write_tag(3u64)?;
use std::ops::Neg;
// CBOR RFC defines this as the bytes of -n -1
let adjusted = self.0.clone().neg().checked_sub(&num_bigint::BigInt::from(1u32)).unwrap().to_biguint().unwrap();
write_bounded_bytes(serializer, &adjusted.to_bytes_be())
},
match sign {
// positive bigint
num_bigint::Sign::Plus |
num_bigint::Sign::NoSign => {
serializer.write_tag(2u64)?;
write_bounded_bytes(serializer, &bytes)
},
// negative bigint
num_bigint::Sign::Minus => {
serializer.write_tag(3u64)?;
use std::ops::Neg;
// CBOR RFC defines this as the bytes of -n -1
let adjusted = self.0.clone().neg().checked_sub(&num_bigint::BigInt::from(1u32)).unwrap().to_biguint().unwrap();
write_bounded_bytes(serializer, &adjusted.to_bytes_be())
},
}
}
},
}
Expand Down Expand Up @@ -772,7 +797,7 @@ impl Deserialize for BigInt {
// uint
CBORType::UnsignedInteger => Ok(Self(num_bigint::BigInt::from(raw.unsigned_integer()?))),
// nint
CBORType::NegativeInteger => Ok(Self(num_bigint::BigInt::from(raw.negative_integer()?))),
CBORType::NegativeInteger => Ok(Self(num_bigint::BigInt::from(read_nint(raw)?))),
_ => return Err(DeserializeFailure::NoVariantMatched.into()),
}
})().map_err(|e| e.annotate("BigInt"))
Expand Down Expand Up @@ -2104,9 +2129,8 @@ mod tests {
assert_eq!(hex::decode("c249010000000000000000").unwrap(), BigInt::from_str("18446744073709551616").unwrap().to_bytes());
// uint
assert_eq!(hex::decode("1b000000e8d4a51000").unwrap(), BigInt::from_str("1000000000000").unwrap().to_bytes());
// nint
// we can't use this due to cbor_event actually not supporting the full NINT spectrum as it uses an i64 for some reason...
//assert_eq!(hex::decode("3bffffffffffffffff").unwrap(), BigInt::from_str("-18446744073709551616").unwrap().to_bytes());
// nint (lowest possible - used to be unsupported but works now)
assert_eq!(hex::decode("3bffffffffffffffff").unwrap(), BigInt::from_str("-18446744073709551616").unwrap().to_bytes());
// this one fits in an i64 though
assert_eq!(hex::decode("3903e7").unwrap(), BigInt::from_str("-1000").unwrap().to_bytes());

Expand Down Expand Up @@ -2318,4 +2342,22 @@ mod tests {
assert_eq!(Int::new_i32(42).as_i32_or_fail().unwrap(), 42);
assert_eq!(Int::new_i32(-42).as_i32_or_fail().unwrap(), -42);
}

#[test]
fn int_full_range() {
// cbor_event's nint API worked via i64 but we now have a workaround for it
// so these tests are here to make sure that workaround works.

// first nint below of i64::MIN
let bytes_x = vec![0x3b, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
let x = Int::from_bytes(bytes_x.clone()).unwrap();
assert_eq!(x.to_str(), "-9223372036854775809");
assert_eq!(bytes_x, x.to_bytes());

// smallest possible nint which is -u64::MAX - 1
let bytes_y = vec![0x3b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff];
let y = Int::from_bytes(bytes_y.clone()).unwrap();
assert_eq!(y.to_str(), "-18446744073709551616");
assert_eq!(bytes_y, y.to_bytes());
}
}

0 comments on commit 3ffa5fa

Please sign in to comment.