diff --git a/js/Redemption.js b/js/Redemption.js new file mode 100644 index 0000000..eac1fc4 --- /dev/null +++ b/js/Redemption.js @@ -0,0 +1,62 @@ +import iconv from 'iconv-lite'; +import RustModule from './RustModule'; +import { newArray, newArray0, copyArray } from './utils/arrays'; +import { apply } from './utils/functions'; + +export const REDEMPTION_PRIVATE_KEY_SIZE = 32; +const MAX_OUTPUT_SIZE = 4096; + +/** + * @param module - the WASM module that is used for crypto operations + * @param redemptionKey - the private redemption key, needs to be {@link REDEMPTION_PRIVATE_KEY_SIZE} + * @param magic - protocol magic integer + * @returns {*} - returns false if the seed is not of the valid length, or returns the redemption address + */ +export const redemptionKeyToAddress = (module, redemptionKey, magic) => { + if (redemptionKey.length !== REDEMPTION_PRIVATE_KEY_SIZE) { + return false; + } + const bufkey = newArray(module, redemptionKey); + const bufaddr = newArray0(module, 1024); + const rs = module.redemption_private_to_address(bufkey, magic, bufaddr); + let result = copyArray(module, bufaddr, rs); + module.dealloc(bufkey); + module.dealloc(bufaddr); + return result; +}; + +/** + * @param module - the WASM module that is used for crypto operations + * @param redemptionKey - the private redemption key, needs to be {@link REDEMPTION_PRIVATE_KEY_SIZE} + * @param input - single input as: { id, index } + * @param output - single output as: { address, value } + * @param magic - protocol magic integer + * @returns {*} - returns false if the seed is not of the valid length, or returns the response as: { cbor_encoded_tx } + */ +export const createRedemptionTransaction = (module, redemptionKey, input, output, magic) => { + if (redemptionKey.length !== REDEMPTION_PRIVATE_KEY_SIZE) { + return false; + } + redemptionKey = [...Buffer.from(redemptionKey)]; + input.id = Buffer.from(input.id).toString('hex'); + const input_obj = { protocol_magic: magic, redemption_key: redemptionKey, input, output }; + const input_str = JSON.stringify(input_obj); + const input_array = iconv.encode(input_str, 'utf8'); + + const bufinput = newArray(module, input_array); + const bufoutput = newArray0(module, MAX_OUTPUT_SIZE); + + let rsz = module.xwallet_redeem(bufinput, input_array.length, bufoutput); + let output_array = copyArray(module, bufoutput, rsz); + + module.dealloc(bufoutput); + module.dealloc(bufinput); + + let output_str = iconv.decode(Buffer.from(output_array), 'utf8'); + return JSON.parse(output_str); +}; + +export default { + redemptionKeyToAddress: apply(redemptionKeyToAddress, RustModule), + createRedemptionTransaction: apply(createRedemptionTransaction, RustModule), +}; diff --git a/js/index.js b/js/index.js index 9efdb4e..eb011a4 100644 --- a/js/index.js +++ b/js/index.js @@ -6,6 +6,7 @@ import Payload from './Payload.js'; import Tx from './Tx.js'; import Config from './Config.js'; import Wallet from './Wallet.js'; +import Redemption from './Redemption.js'; import RandomAddressChecker from './RandomAddressChecker'; import PasswordProtect from './PasswordProtect'; @@ -19,6 +20,7 @@ module.exports = { RandomAddressChecker, HdWallet, Wallet, + Redemption, Config, PasswordProtect, }; diff --git a/js/tests/redemption.js b/js/tests/redemption.js new file mode 100644 index 0000000..c5fc816 --- /dev/null +++ b/js/tests/redemption.js @@ -0,0 +1,82 @@ +const expect = require('chai').expect; +const CardanoCrypto = require('../../dist/index.js'); +const bs58 = require('bs58'); +const cbor = require('cbor'); +const crc = require('crc'); + +const TEST_VECTORS = [ + { + redemptionKey: Buffer.from('qXQWDxI3JrlFRtC4SeQjeGzLbVXWBomYPbNO1Vfm1T4=', 'base64'), + expectedAddress: 'Ae2tdPwUPEZ1xZTLczMGYL5PhADi1nbFmESqS9vUuLkyUe1isQ77TRUE9NS', + txId: new Uint8Array([0xaa,0xd7,0x8a,0x13,0xb5,0x0a,0x01,0x4a,0x24,0x63,0x3c,0x7d,0x44,0xfd,0x8f,0x8d,0x18,0xf6,0x7b,0xbb,0x3f,0xa9,0xcb,0xce,0xdf,0x83,0x4a,0xc8,0x99,0x75,0x9d,0xcd]), + txOutIndex: 1, + coinValue: 12345678 + } +]; + +let mkTest = (i) => { + const { redemptionKey, expectedAddress, txId, txOutIndex, coinValue } = TEST_VECTORS[i]; + const cfg = CardanoCrypto.Config.defaultConfig(); + + describe('Test ' + i, function() { + before(async () => { + await CardanoCrypto.loadRustModule() + }); + + it('generates valid address', function() { + const a = CardanoCrypto.Redemption.redemptionKeyToAddress(redemptionKey, cfg.protocol_magic); + const [tagged, checksum] = cbor.decode(Buffer.from(a)); + expect(crc.crc32(tagged.value)).equal(checksum); + }); + + it('creates address matching expected', function() { + const a = CardanoCrypto.Redemption.redemptionKeyToAddress(redemptionKey, cfg.protocol_magic); + expect(bs58.encode(Buffer.from(a))).equal(expectedAddress) + }); + + it('generates valid transaction', function () { + const address = CardanoCrypto.Redemption.redemptionKeyToAddress(redemptionKey, cfg.protocol_magic); + const input = { id: txId, index: txOutIndex }; + const output = { address: bs58.encode(Buffer.from(address)), value: JSON.stringify(coinValue) }; + const { result: { cbor_encoded_tx } } = CardanoCrypto.Redemption.createRedemptionTransaction(redemptionKey, input, output, cfg.protocol_magic); + + // destruct result transaction + const [[resultInputs, resultOutputs, attributes], resultWitnesses] = cbor.decode(Buffer.from(cbor_encoded_tx)); + + // validate inputs + expect(resultInputs.length).equal(1); + expect(resultInputs[0].length).equal(2); + const [[intputType, inputTagged]] = resultInputs; + expect(intputType).equal(0); + const [inputId, inputIndex] = cbor.decode(inputTagged.value); + expect(inputIndex).equal(txOutIndex); + expect(inputId).deep.equal(txId); + + // validate outputs + expect(resultInputs.length).equal(1); + expect(resultInputs[0].length).equal(2); + const [[outputAddress, outputValue]] = resultOutputs; + expect(cbor.encode(outputAddress)).deep.equal(address); + expect(outputValue).equal(coinValue); + + // validate witness + expect(resultWitnesses.length).equal(1); + expect(resultWitnesses[0].length).equal(2); + const [[witnessType, witnessTagged]] = resultWitnesses; + expect(witnessType).equal(2); + const [witnessPub, witnessSign] = cbor.decode(witnessTagged.value); + + // TODO: expecting fake witness data - fix after implementing signing in Rust + expect(witnessPub.toString('hex')) + .equal('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'); + expect(witnessSign.toString('hex')) + .equal('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'); + }); + }); +}; + +describe('Test redemption', function() { + for (let i = 0; i < TEST_VECTORS.length; i++) { + mkTest(i); + } +}); diff --git a/package.json b/package.json index aec49cd..82d4369 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,10 @@ "mocha": "5.0.2", "nodemon": "1.17.1", "wasm-loader": "1.3.0", - "webpack": "^3.11.0" + "webpack": "^3.11.0", + "bs58": "4.0.1", + "cbor": "4.1.4", + "crc": "3.8.0" }, "peerDependencies": { "bip39": "2.3.0" diff --git a/wallet-wasm/src/lib.rs b/wallet-wasm/src/lib.rs index c17465d..1abd4e1 100644 --- a/wallet-wasm/src/lib.rs +++ b/wallet-wasm/src/lib.rs @@ -25,6 +25,7 @@ use self::cardano::wallet::{ scheme::{SelectionPolicy, Wallet}, }; use self::cardano::{coin, fee, tx, txutils, util::hex}; +use self::cardano::{redeem, txbuild}; use self::cardano::util::try_from_slice::TryFromSlice; @@ -1254,3 +1255,87 @@ pub extern "C" fn decrypt_with_password( -1 } } + +#[no_mangle] +pub extern "C" fn redemption_private_to_address( + private_ptr: *const c_uchar, + protocol_magic: u32, + out: *mut c_uchar, +) -> u32 { + let priv_key = unsafe { + let slice: &[u8] = std::slice::from_raw_parts(private_ptr, redeem::PRIVATEKEY_SIZE); + redeem::PrivateKey::from_slice(slice).unwrap() + }; + let pub_key = priv_key.public(); + let magic = cardano::config::ProtocolMagic::from(protocol_magic); + let (_, address) = tx::redeem_pubkey_to_txid(&pub_key, magic); + let address_bytes = cbor!(address).unwrap(); + unsafe { write_data(&address_bytes, out) } + return address_bytes.len() as u32; +} + +#[derive(Serialize, Deserialize, Debug)] +struct WalletRedeemInput { + protocol_magic: cardano::config::ProtocolMagic, + redemption_key: [u8; redeem::PRIVATEKEY_SIZE], // hex + input: TxIn, + output: TxOut, +} + +#[derive(Serialize, Deserialize, Debug)] +struct WalletRedeemOutput { + cbor_encoded_tx: Vec, +} + +#[no_mangle] +pub extern "C" fn xwallet_redeem( + input_ptr: *const c_uchar, + input_sz: usize, + output_ptr: *mut c_uchar, +) -> i32 { + let data: WalletRedeemInput = input_json!(output_ptr, input_ptr, input_sz); + let mut txbuilder = txbuild::TxBuilder::new(); + txbuilder.add_input(&data.input.convert(), data.output.value.0); + txbuilder.add_output_value(&data.output.convert()); + let tx: tx::Tx = jrpc_try!( + output_ptr, + txbuilder.make_tx() + ); + print!("Tx: {}", tx); + let redemption_key = jrpc_try!( + output_ptr, + redeem::PrivateKey::from_slice(&data.redemption_key) + ); + print!("Key: {}", redemption_key); + let witness = jrpc_try!( + output_ptr, + create_redemption_witness(data.protocol_magic, &redemption_key, &tx.id()) + ); + let mut finalized = txbuild::TxFinalized::new(tx); + jrpc_try!( + output_ptr, + finalized.add_witness(witness) + ); + let txaux: tx::TxAux = jrpc_try!( + output_ptr, + finalized.make_txaux() + ); + let cbor = jrpc_try!(output_ptr, cbor!(&txaux)); + jrpc_ok!(output_ptr, WalletRedeemOutput { + cbor_encoded_tx: cbor + }) +} + +fn create_redemption_witness( + protocol_magic: cardano::config::ProtocolMagic, + key: &redeem::PrivateKey, + txid: &tx::TxId, +) -> redeem::Result { + // TODO: actual implementation + let s32 = (0..64).map(|_| "f").collect::(); + let s64 = (0..128).map(|_| "f").collect::(); + let pk = redeem::PublicKey::from_hex(&s32); + let sg = redeem::Signature::from_hex(&s64); + return pk.and_then(|k| sg.map(|s| (k, s))) + .map(|(k,s)| tx::TxInWitness::RedeemWitness(k, s)); +} \ No newline at end of file