Skip to content

Commit

Permalink
feat: added signing options with extra signers to the transaction fin…
Browse files Browse the repository at this point in the history
…alize method
  • Loading branch information
AngelCastilloB committed Aug 31, 2022
1 parent 61ae63f commit 514b718
Show file tree
Hide file tree
Showing 20 changed files with 288 additions and 114 deletions.
168 changes: 106 additions & 62 deletions packages/core/src/CSL/coreToCsl/coreToCsl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
MultiAsset,
NativeScript,
NativeScripts,
PlutusScripts,
PublicKey,
RewardAddress,
ScriptAll,
Expand Down Expand Up @@ -176,14 +177,103 @@ export const txMint = (mint: Cardano.TokenMap) => {
return cslMint;
};

export const nativeScript = (script: Cardano.NativeScript): NativeScript => {
let cslScript: NativeScript;
const kind = script.kind;

switch (kind) {
case Cardano.NativeScriptKind.RequireSignature: {
cslScript = NativeScript.new_script_pubkey(
ScriptPubkey.new(Ed25519KeyHash.from_bytes(Buffer.from(script.keyHash, 'hex')))
);
break;
}
case Cardano.NativeScriptKind.RequireAllOf: {
const cslScripts = NativeScripts.new();
for (const subscript of script.scripts) {
cslScripts.add(nativeScript(subscript));
}
cslScript = NativeScript.new_script_all(ScriptAll.new(cslScripts));
break;
}
case Cardano.NativeScriptKind.RequireAnyOf: {
const cslScripts2 = NativeScripts.new();
for (const subscript of script.scripts) {
cslScripts2.add(nativeScript(subscript));
}
cslScript = NativeScript.new_script_any(ScriptAny.new(cslScripts2));
break;
}
case Cardano.NativeScriptKind.RequireMOf: {
const cslScripts3 = NativeScripts.new();
for (const subscript of script.scripts) {
cslScripts3.add(nativeScript(subscript));
}
cslScript = NativeScript.new_script_n_of_k(ScriptNOfK.new(script.required, cslScripts3));
break;
}
case Cardano.NativeScriptKind.RequireTimeBefore: {
cslScript = NativeScript.new_timelock_expiry(
TimelockExpiry.new_timelockexpiry(BigNum.from_str(script.slot.toString()))
);
break;
}
case Cardano.NativeScriptKind.RequireTimeAfter: {
cslScript = NativeScript.new_timelock_start(
TimelockStart.new_timelockstart(BigNum.from_str(script.slot.toString()))
);
break;
}
default:
throw new SerializationError(
SerializationFailure.InvalidNativeScriptKind,
`Native Script Type value '${kind}' is not supported.`
);
}

return cslScript;
};

export const getScripts = (
scripts: Cardano.Script[]
): { nativeScripts: NativeScripts; plutusScripts: PlutusScripts } => {
const nativeScripts: NativeScripts = NativeScripts.new();
const plutusScripts: PlutusScripts = PlutusScripts.new();

for (const script of scripts) {
switch (script.__type) {
case Cardano.ScriptType.Native:
nativeScripts.add(nativeScript(script));
break;
// TODO: add support for Plutus scripts.
case Cardano.ScriptType.Plutus:
default:
throw new SerializationError(
SerializationFailure.InvalidScriptType,
`Script Type value '${script.__type}' is not supported.`
);
}
}

return { nativeScripts, plutusScripts };
};

export const txAuxiliaryData = (auxiliaryData?: Cardano.AuxiliaryData): AuxiliaryData | undefined => {
if (!auxiliaryData) return;
const result = AuxiliaryData.new();
// TODO: add support for auxiliaryData.scripts
const { blob } = auxiliaryData.body;

const { blob, scripts } = auxiliaryData.body;
if (blob) {
result.set_metadata(txMetadata(blob));
}

if (scripts) {
const { nativeScripts, plutusScripts } = getScripts(scripts);

result.set_native_scripts(nativeScripts);
result.set_plutus_scripts(plutusScripts);
}

return result;
};

Expand Down Expand Up @@ -241,6 +331,7 @@ export const txBody = (
BigNum.from_str(fee.toString()),
validityInterval.invalidHereafter
);

if (validityInterval.invalidBefore) {
cslBody.set_validity_start_interval(validityInterval.invalidBefore);
}
Expand Down Expand Up @@ -270,80 +361,33 @@ export const txBody = (
if (cslAuxiliaryData) {
cslBody.set_auxiliary_data_hash(hash_auxiliary_data(cslAuxiliaryData));
}

return cslBody;
};

export const witnessSet = (signatures: Cardano.Signatures): TransactionWitnessSet => {
export const witnessSet = (witness: Cardano.Witness): TransactionWitnessSet => {
const cslWitnessSet = TransactionWitnessSet.new();
const vkeyWitnesses = Vkeywitnesses.new();
for (const [vkey, signature] of signatures.entries()) {

if (witness.scripts) {
const { nativeScripts, plutusScripts } = getScripts(witness.scripts);

cslWitnessSet.set_native_scripts(nativeScripts);
cslWitnessSet.set_plutus_scripts(plutusScripts);
}

for (const [vkey, signature] of witness.signatures.entries()) {
const publicKey = PublicKey.from_bytes(Buffer.from(vkey, 'hex'));
const vkeyWitness = Vkeywitness.new(Vkey.new(publicKey), Ed25519Signature.from_hex(signature.toString()));
vkeyWitnesses.add(vkeyWitness);
}
cslWitnessSet.set_vkeys(vkeyWitnesses);

return cslWitnessSet;
};

export const tx = ({ body, witness, auxiliaryData }: Cardano.NewTxAlonzo): Transaction => {
const txWitnessSet = witnessSet(witness.signatures);
const txWitnessSet = witnessSet(witness);
// Possible optimization: only convert auxiliary data once
return Transaction.new(txBody(body, auxiliaryData), txWitnessSet, txAuxiliaryData(auxiliaryData));
};

export const nativeScript = (script: Cardano.NativeScript): NativeScript => {
let cslScript: NativeScript;
const kind = script.kind;

switch (kind) {
case Cardano.NativeScriptKind.RequireSignature: {
cslScript = NativeScript.new_script_pubkey(
ScriptPubkey.new(Ed25519KeyHash.from_bytes(Buffer.from(script.keyHash, 'hex')))
);
break;
}
case Cardano.NativeScriptKind.RequireAllOf: {
const cslScripts = NativeScripts.new();
for (const subscript of script.scripts) {
cslScripts.add(nativeScript(subscript));
}
cslScript = NativeScript.new_script_all(ScriptAll.new(cslScripts));
break;
}
case Cardano.NativeScriptKind.RequireAnyOf: {
const cslScripts2 = NativeScripts.new();
for (const subscript of script.scripts) {
cslScripts2.add(nativeScript(subscript));
}
cslScript = NativeScript.new_script_any(ScriptAny.new(cslScripts2));
break;
}
case Cardano.NativeScriptKind.RequireMOf: {
const cslScripts3 = NativeScripts.new();
for (const subscript of script.scripts) {
cslScripts3.add(nativeScript(subscript));
}
cslScript = NativeScript.new_script_n_of_k(ScriptNOfK.new(script.required, cslScripts3));
break;
}
case Cardano.NativeScriptKind.RequireTimeBefore: {
cslScript = NativeScript.new_timelock_expiry(
TimelockExpiry.new_timelockexpiry(BigNum.from_str(script.slot.toString()))
);
break;
}
case Cardano.NativeScriptKind.RequireTimeAfter: {
cslScript = NativeScript.new_timelock_start(
TimelockStart.new_timelockstart(BigNum.from_str(script.slot.toString()))
);
break;
}
default:
throw new SerializationError(
SerializationFailure.InvalidNativeScriptKind,
`Native Script Type value '${kind}' is not supported.`
);
}

return cslScript;
};
3 changes: 2 additions & 1 deletion packages/core/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export enum SerializationFailure {
Overflow = 'OVERFLOW',
InvalidAddress = 'INVALID_ADDRESS',
MaxLengthLimit = 'MAX_LENGTH_LIMIT',
InvalidNativeScriptKind = 'INVALID_NATIVE_SCRIPT_KIND'
InvalidNativeScriptKind = 'INVALID_NATIVE_SCRIPT_KIND',
InvalidScriptType = 'INVALID_SCRIPT_TYPE'
}

export class SerializationError extends CustomError {
Expand Down
4 changes: 2 additions & 2 deletions packages/e2e/test/cardano-services/load/load.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ describe('load', () => {
});

logger.info(`Fragmentation tx: ${tx.hash}`);
await wallet.submitTx(await wallet.finalizeTx(tx));
await wallet.submitTx(await wallet.finalizeTx({ tx }));
await waitForTxInBlockchain(wallet, tx.hash);
logger.info('Fragmentation completed');
};
Expand Down Expand Up @@ -290,7 +290,7 @@ describe('load', () => {

const finalizeAndSubmit = async (wallet: ObservableWallet, tx: InitializeTxResult) => {
try {
await wallet.submitTx(await wallet.finalizeTx(tx));
await wallet.submitTx(await wallet.finalizeTx({ tx }));
logger.info(`Submitted tx: ${tx.hash}`);
} catch (error) {
logger.error(JSONBig.stringify(tx), error);
Expand Down
2 changes: 1 addition & 1 deletion packages/e2e/test/local-network/local-network.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ describe('Local Network', () => {
outputs: new Set([{ address: receivingAddress, value: { coins: tAdaToSend } }])
});

const signedTx = await wallet1.finalizeTx(unsignedTx);
const signedTx = await wallet1.finalizeTx({ tx: unsignedTx });
await wallet1.submitTx(signedTx);

// Wait until wallet two is aware of the funds.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ describe('SingleAddressWallet/delegation', () => {
certificates,
outputs: new Set([{ address: destAddresses, value: { coins: tx1OutputCoins } }])
});
await sourceWallet.submitTx(await sourceWallet.finalizeTx(tx1Internals));
await sourceWallet.submitTx(await sourceWallet.finalizeTx({ tx: tx1Internals }));

// Test it locks available balance after tx is submitted
await firstValueFromTimed(
Expand Down Expand Up @@ -168,7 +168,7 @@ describe('SingleAddressWallet/delegation', () => {
}
]
});
await sourceWallet.submitTx(await sourceWallet.finalizeTx(tx2Internals));
await sourceWallet.submitTx(await sourceWallet.finalizeTx({ tx: tx2Internals }));
await waitForTx(sourceWallet, tx2Internals);
const tx2ConfirmedState = await getWalletStateSnapshot(sourceWallet);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('SingleAddressWallet/metadata', () => {
auxiliaryData,
outputs: new Set([{ address: ownAddress, value: { coins: minimumCoin } }])
});
const outgoingTx = await wallet.finalizeTx(txInternals, auxiliaryData);
const outgoingTx = await wallet.finalizeTx({ auxiliaryData, tx: txInternals });
await wallet.submitTx(outgoingTx);
const loadedTx = await firstValueFrom(
wallet.transactions.history$.pipe(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('SingleAddressWallet', () => {
outputs: new Set([{ address, value: { coins: moreThanHalfOfTheBalanceCoins } }])
});

const finalizedTx1 = await wallet.finalizeTx(tx1);
const finalizedTx1 = await wallet.finalizeTx({ tx: tx1 });
await wallet.submitTx(finalizedTx1);

const tx2 = await wallet.initializeTx({
Expand All @@ -43,7 +43,7 @@ describe('SingleAddressWallet', () => {
const usingTx1OutputAsInput = [...tx2.inputSelection.inputs].some(([txIn]) => txIn.txId === finalizedTx1.id);
expect(usingTx1OutputAsInput).toBe(true);

const finalizedTx2 = await wallet.finalizeTx(tx2);
const finalizedTx2 = await wallet.finalizeTx({ tx: tx2 });

try {
await wallet.submitTx(finalizedTx2);
Expand Down
2 changes: 1 addition & 1 deletion packages/e2e/test/web-extension/extension/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,6 @@ document.querySelector('#buildAndSignTx')!.addEventListener('click', async () =>
}
])
});
const signedTx = await wallet.finalizeTx(tx);
const signedTx = await wallet.finalizeTx({ tx });
document.querySelector('#signature')!.textContent = signedTx.witness.signatures.values().next().value;
});
28 changes: 28 additions & 0 deletions packages/wallet/src/KeyManagement/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,31 @@ export interface KeyAgent {
export type AsyncKeyAgent = Pick<KeyAgent, 'deriveAddress' | 'derivePublicKey' | 'signBlob' | 'signTransaction'> & {
knownAddresses$: Observable<GroupedAddress[]>;
} & Shutdown;

/**
* The result of the transaction signer signing operation.
*/
export type TransactionSignerResult = {
/**
* The public key matching the private key that generate the signautre.
*/
pubKey: Cardano.Ed25519PublicKey;

/**
* The transaction signature.
*/
signature: Cardano.Ed25519Signature;
};

/**
* Produces a Ed25519Signature of a transaction.
*/
export interface TransactionSigner {
/**
* Sings a transaction.
*
* @param tx The transaction to be signed.
* @returns A Ed25519 transaction signature.
*/
sign(tx: TxInternals): Promise<TransactionSignerResult>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { AccountKeyDerivationPath, KeyAgent, TransactionSigner, TransactionSignerResult } from '../types';
import { Cardano } from '@cardano-sdk/core';
import { ProofGenerationError } from '../errors';
import { TxInternals } from '../../Transaction';

const EXPECTED_SIG_NUM = 1;

/**
* Generates a Ed25519Signature of a transaction using a key agent.
*/
export class KeyAgentTransactionSigner implements TransactionSigner {
#keyAgent: KeyAgent;
#account: AccountKeyDerivationPath;

/**
* Initializes a new instance of the KeyAgentTransactionSigner class.
*
* @param keyAgent The key agent that will produce the signature.
* @param account The account derivation path of the key to be used to generate the signature.
* @class
*/
constructor(keyAgent: KeyAgent, account: AccountKeyDerivationPath) {
this.#keyAgent = keyAgent;
this.#account = account;
}

/**
* Sings a transaction.
*
* @param tx The transaction to be signed.
* @returns A Ed25519 transaction signature.
*/
async sign(tx: TxInternals): Promise<TransactionSignerResult> {
const signatures: Cardano.Signatures = await this.#keyAgent.signTransaction(tx, {
additionalKeyPaths: [this.#account]
});

if (signatures.size !== EXPECTED_SIG_NUM)
throw new ProofGenerationError(
`Invalid number of signatures. Expected ${EXPECTED_SIG_NUM} and got ${signatures.size}`
);

const [pubKey] = signatures.keys();
const [signature] = signatures.values();

return { pubKey, signature };
}
}
1 change: 1 addition & 0 deletions packages/wallet/src/KeyManagement/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './cachedGetPassword';
export * from './stubSignTransaction';
export * from './mapHardwareSigningData';
export * from './createAsyncKeyAgent';
export * from './KeyAgentTransactionSigner';
Loading

0 comments on commit 514b718

Please sign in to comment.