From ef6ffb4ee8bd69cff5f5709f78a88825857e8673 Mon Sep 17 00:00:00 2001 From: vetalcore Date: Wed, 10 Apr 2024 12:25:24 +0100 Subject: [PATCH] feat (hardware-ledger): sign new certificate types --- .../hardware-ledger/src/LedgerKeyAgent.ts | 4 +- .../src/transformers/certificates.ts | 317 +++++++--- .../hardware-ledger/src/transformers/index.ts | 1 + .../hardware-ledger/src/transformers/tx.ts | 2 +- .../src/transformers/withdrawals.ts | 2 +- packages/hardware-ledger/test/testData.ts | 105 +++- .../test/transformers/certificates.test.ts | 595 ++++++++++++++++-- .../test/transformers/tx.test.ts | 169 ++++- .../hardware/ledger/LedgerKeyAgent.test.ts | 328 +++++++++- 9 files changed, 1374 insertions(+), 149 deletions(-) diff --git a/packages/hardware-ledger/src/LedgerKeyAgent.ts b/packages/hardware-ledger/src/LedgerKeyAgent.ts index d01e8c7503d..970889bc99c 100644 --- a/packages/hardware-ledger/src/LedgerKeyAgent.ts +++ b/packages/hardware-ledger/src/LedgerKeyAgent.ts @@ -11,7 +11,8 @@ import { SerializableLedgerKeyAgentData, SignBlobResult, SignTransactionContext, - errors + errors, + util } from '@cardano-sdk/key-management'; import { HID } from 'node-hid'; import { LedgerDevice, LedgerTransportType } from './types'; @@ -540,6 +541,7 @@ export class LedgerKeyAgent extends KeyAgentBase { const ledgerTxData = await toLedgerTx(body, { accountIndex: this.accountIndex, chainId: this.chainId, + dRepPublicKey: await this.derivePublicKey(util.DREP_KEY_DERIVATION_PATH), knownAddresses, txInKeyPathMap }); diff --git a/packages/hardware-ledger/src/transformers/certificates.ts b/packages/hardware-ledger/src/transformers/certificates.ts index dbf84c4b621..79b42aa7864 100644 --- a/packages/hardware-ledger/src/transformers/certificates.ts +++ b/packages/hardware-ledger/src/transformers/certificates.ts @@ -1,140 +1,155 @@ +/* eslint-disable complexity */ +import * as Crypto from '@cardano-sdk/crypto'; import * as Ledger from '@cardano-foundation/ledgerjs-hw-app-cardano'; import { Cardano } from '@cardano-sdk/core'; import { GroupedAddress, util } from '@cardano-sdk/key-management'; import { InvalidArgumentError, Transform, areStringsEqualInConstantTime } from '@cardano-sdk/util'; import { LedgerTxTransformerContext } from '../types'; -type StakeKeyCertificateType = Ledger.CertificateType.STAKE_REGISTRATION | Ledger.CertificateType.STAKE_DEREGISTRATION; - -type StakeKeyCertificate = { - params: { - stakeCredential: Ledger.CredentialParams; - }; - type: StakeKeyCertificateType; -}; - -// Type guard for certificates with stakeCredential -const hasStakeCredential = ( - certificate: Cardano.StakeAddressCertificate | Cardano.StakeDelegationCertificate | Cardano.PoolRetirementCertificate -): certificate is Cardano.StakeAddressCertificate | Cardano.StakeDelegationCertificate => - 'stakeCredential' in certificate; - -/** - * This function checks if the provided certificate contains a stake credential and if so, attempts to - * find a corresponding known address within the provided context based on a constant-time string comparison - * of the hashed reward account and the stake credential hash. - * - * @param {Cardano.StakeAddressCertificate | Cardano.StakeDelegationCertificate | Cardano.PoolRetirementCertificate} certificate - The certificate containing a stake credential. - * @param {LedgerTxTransformerContext} [context] - The context containing known addresses to search within. Optional; if not provided, the function returns undefined. - * @returns {GroupedAddress | undefined} The matching grouped address if found, or undefined if no match is found or if context is not provided. - */ -export const getKnownAddress = ( - certificate: Cardano.StakeAddressCertificate | Cardano.StakeDelegationCertificate | Cardano.PoolRetirementCertificate, - context?: LedgerTxTransformerContext -): GroupedAddress | undefined => - !!context && hasStakeCredential(certificate) - ? context?.knownAddresses.find((address) => - areStringsEqualInConstantTime( - Cardano.RewardAccount.toHash(address.rewardAccount) as unknown as string, - certificate.stakeCredential.hash as unknown as string - ) - ) - : undefined; - -const getStakeAddressCertificate = ( - certificate: Cardano.StakeAddressCertificate, - context: LedgerTxTransformerContext, - type: StakeKeyCertificateType -): StakeKeyCertificate => { - const knownAddress = getKnownAddress(certificate, context); - - const rewardAddress = knownAddress ? Cardano.Address.fromBech32(knownAddress.rewardAccount)?.asReward() : null; - const path = util.stakeKeyPathFromGroupedAddress(knownAddress); - const credentialType = rewardAddress - ? rewardAddress.getPaymentCredential().type - : Ledger.CredentialParamsType.SCRIPT_HASH; +const mapAnchorToParams = (certificate: Cardano.Certificate) => ({ + ...('anchor' in certificate && + certificate?.anchor && { anchor: { hashHex: certificate.anchor.dataHash, url: certificate.anchor.url } }) +}); - let credential: Ledger.CredentialParams; +const credentialMapper = ( + credential: Cardano.Credential, + credentialType: Cardano.CredentialType | Ledger.CredentialParamsType.SCRIPT_HASH, + path: Crypto.BIP32Path | null +): Ledger.CredentialParams => { + let credentialParams: Ledger.CredentialParams; switch (credentialType) { case Cardano.CredentialType.KeyHash: { - credential = path + credentialParams = path ? { keyPath: path, type: Ledger.CredentialParamsType.KEY_PATH } : { - keyHashHex: certificate.stakeCredential.hash, + keyHashHex: credential.hash, type: Ledger.CredentialParamsType.KEY_HASH }; break; } case Cardano.CredentialType.ScriptHash: default: { - credential = { - scriptHashHex: certificate.stakeCredential.hash, + credentialParams = { + scriptHashHex: credential.hash, type: Ledger.CredentialParamsType.SCRIPT_HASH }; } } - return { - params: { - stakeCredential: credential - }, - type - }; + return credentialParams; }; -export const stakeDelegationCertificate: Transform< - Cardano.StakeDelegationCertificate, - Ledger.Certificate, - LedgerTxTransformerContext -> = (certificate, context): Ledger.Certificate => { - const poolIdKeyHash = Cardano.PoolId.toKeyHash(certificate.poolId); - const knownAddress = getKnownAddress(certificate, context); - - const rewardAddress = knownAddress ? Cardano.Address.fromBech32(knownAddress.rewardAccount)?.asReward() : null; - - const credentialType = rewardAddress - ? rewardAddress.getPaymentCredential().type - : Ledger.CredentialParamsType.SCRIPT_HASH; - - const path = util.stakeKeyPathFromGroupedAddress(knownAddress); - - let credential: Ledger.CredentialParams; +const drepParamsMapper = ( + drep: Cardano.Credential, + credentialType: Cardano.CredentialType | Ledger.CredentialParamsType.SCRIPT_HASH, + path: Crypto.BIP32Path | null +): Ledger.KeyPathDRepParams | Ledger.KeyHashDRepParams | Ledger.ScriptHashDRepParams => { + let dRepParams: Ledger.DRepParams; switch (credentialType) { case Cardano.CredentialType.KeyHash: { - credential = path + dRepParams = path ? { keyPath: path, - type: Ledger.CredentialParamsType.KEY_PATH + type: Ledger.DRepParamsType.KEY_PATH } : { - keyHashHex: certificate.stakeCredential.hash, - type: Ledger.CredentialParamsType.KEY_HASH + keyHashHex: drep.hash, + type: Ledger.DRepParamsType.KEY_HASH }; break; } case Cardano.CredentialType.ScriptHash: default: { - credential = { - scriptHashHex: certificate.stakeCredential.hash, - type: Ledger.CredentialParamsType.SCRIPT_HASH + dRepParams = { + scriptHashHex: drep.hash, + type: Ledger.DRepParamsType.SCRIPT_HASH }; } } - return { - params: { - poolKeyHashHex: poolIdKeyHash, - stakeCredential: credential - }, - type: Ledger.CertificateType.STAKE_DELEGATION - }; + return dRepParams; }; +/** + * This function attempts to find a corresponding known address within the provided context based + * on a constant-time string comparison of the hashed reward account and the stake credential hash. + * + * @param {Cardano.Credential} credential - Stake credential. + * @param {LedgerTxTransformerContext} [context] - The context containing known addresses to search within. Optional; if not provided, the function returns undefined. + * @returns {GroupedAddress | undefined} The matching grouped address if found, or undefined if no match is found or if context is not provided. + */ +export const getKnownAddress = ( + credential: Cardano.Credential, + context?: LedgerTxTransformerContext +): GroupedAddress | undefined => + context + ? context?.knownAddresses.find((address) => + areStringsEqualInConstantTime( + Cardano.RewardAccount.toHash(address.rewardAccount) as unknown as string, + credential.hash as unknown as string + ) + ) + : undefined; + +const getCredentialType = (knownAddress: GroupedAddress | undefined) => { + const rewardAddress = knownAddress ? Cardano.Address.fromBech32(knownAddress.rewardAccount)?.asReward() : null; + return rewardAddress ? rewardAddress.getPaymentCredential().type : Ledger.CredentialParamsType.SCRIPT_HASH; +}; + +const stakeCredentialMapper = (credential: Cardano.Credential, context: LedgerTxTransformerContext) => { + const knownAddress = getKnownAddress(credential, context); + const credentialType = getCredentialType(knownAddress); + const path = util.stakeKeyPathFromGroupedAddress(knownAddress); + + return credentialMapper(credential, credentialType, path); +}; + +const getStakeAddressCertificate: Transform< + Cardano.StakeAddressCertificate, + Ledger.Certificate, + LedgerTxTransformerContext +> = (certificate, context): Ledger.Certificate => ({ + params: { + stakeCredential: stakeCredentialMapper(certificate.stakeCredential, context!) + }, + type: + certificate.__typename === Cardano.CertificateType.StakeRegistration + ? Ledger.CertificateType.STAKE_REGISTRATION + : Ledger.CertificateType.STAKE_DEREGISTRATION +}); + +const getNewStakeAddressCertificate: Transform< + Cardano.NewStakeAddressCertificate, + Ledger.Certificate, + LedgerTxTransformerContext +> = (certificate, context): Ledger.Certificate => ({ + params: { + deposit: certificate.deposit, + stakeCredential: stakeCredentialMapper(certificate.stakeCredential, context!) + }, + type: + certificate.__typename === Cardano.CertificateType.Registration + ? Ledger.CertificateType.STAKE_REGISTRATION_CONWAY + : Ledger.CertificateType.STAKE_DEREGISTRATION_CONWAY +}); + +export const stakeDelegationCertificate: Transform< + Cardano.StakeDelegationCertificate, + Ledger.Certificate, + LedgerTxTransformerContext +> = (certificate, context): Ledger.Certificate => ({ + params: { + poolKeyHashHex: Cardano.PoolId.toKeyHash(certificate.poolId), + stakeCredential: stakeCredentialMapper(certificate.stakeCredential, context!) + }, + type: Ledger.CertificateType.STAKE_DELEGATION +}); + const toPoolMetadata: Transform = (metadataJson) => ({ metadataHashHex: metadataJson.hash, metadataUrl: metadataJson.url @@ -268,28 +283,136 @@ const poolRetirementCertificate: Transform< }; }; -const toCert = (cert: Cardano.Certificate, context: LedgerTxTransformerContext) => { +const checkDrepPublicKeyAgainstCredential = async ( + dRepPublicKey: Crypto.Ed25519PublicKeyHex | undefined, + hash: Crypto.Hash28ByteBase16 +) => { + if ( + !dRepPublicKey || + (await Crypto.Ed25519PublicKey.fromHex(dRepPublicKey).hash()).hex() !== Crypto.Ed25519KeyHashHex(hash) + ) { + throw new InvalidArgumentError('certificate', 'dRepPublicKey does not match certificate drep credential.'); + } +}; + +const drepRegistrationCertificate: Transform< + Cardano.RegisterDelegateRepresentativeCertificate | Cardano.UnRegisterDelegateRepresentativeCertificate, + Promise, + LedgerTxTransformerContext +> = async (certificate, context): Promise => { + if (!context) throw new InvalidArgumentError('LedgerTxTransformerContext', 'values was not provided'); + await checkDrepPublicKeyAgainstCredential(context?.dRepPublicKey, certificate.dRepCredential.hash); + + const params: Ledger.DRepRegistrationParams = { + ...mapAnchorToParams(certificate), + dRepCredential: credentialMapper( + certificate.dRepCredential, + certificate.dRepCredential.type, + util.accountKeyDerivationPathToBip32Path(context.accountIndex, util.DREP_KEY_DERIVATION_PATH) + ), + deposit: certificate.deposit + }; + + return { + params, + type: + certificate.__typename === Cardano.CertificateType.RegisterDelegateRepresentative + ? Ledger.CertificateType.DREP_REGISTRATION + : Ledger.CertificateType.DREP_DEREGISTRATION + }; +}; + +const updateDRepCertificate: Transform< + Cardano.UpdateDelegateRepresentativeCertificate, + Promise, + LedgerTxTransformerContext +> = async (certificate, context): Promise => { + if (!context) throw new InvalidArgumentError('LedgerTxTransformerContext', 'values was not provided'); + + await checkDrepPublicKeyAgainstCredential(context?.dRepPublicKey, certificate.dRepCredential.hash); + + const params: Ledger.DRepUpdateParams = { + ...mapAnchorToParams(certificate), + dRepCredential: credentialMapper( + certificate.dRepCredential, + certificate.dRepCredential.type, + util.accountKeyDerivationPathToBip32Path(context.accountIndex, util.DREP_KEY_DERIVATION_PATH) + ) + }; + + return { + params, + type: Ledger.CertificateType.DREP_UPDATE + }; +}; + +const drepMapper = (drep: Cardano.DelegateRepresentative, context: LedgerTxTransformerContext): Ledger.DRepParams => { + if (Cardano.isDRepAlwaysAbstain(drep)) { + return { + type: Ledger.DRepParamsType.ABSTAIN + }; + } else if (Cardano.isDRepAlwaysNoConfidence(drep)) { + return { + type: Ledger.DRepParamsType.NO_CONFIDENCE + }; + } else if (Cardano.isDRepCredential(drep)) { + return drepParamsMapper( + drep, + drep.type, + util.accountKeyDerivationPathToBip32Path(context.accountIndex, util.DREP_KEY_DERIVATION_PATH) + ); + } + throw new Error('incorrect drep supplied'); +}; + +export const voteDelegationCertificate: Transform< + Cardano.VoteDelegationCertificate, + Ledger.Certificate, + LedgerTxTransformerContext +> = (certificate, context): Ledger.Certificate => ({ + params: { + dRep: drepMapper(certificate.dRep, context!), + stakeCredential: stakeCredentialMapper(certificate.stakeCredential, context!) + }, + type: Ledger.CertificateType.VOTE_DELEGATION +}); + +const toCert = async (cert: Cardano.Certificate, context: LedgerTxTransformerContext): Promise => { switch (cert.__typename) { case Cardano.CertificateType.StakeRegistration: - return getStakeAddressCertificate(cert, context, Ledger.CertificateType.STAKE_REGISTRATION); + return getStakeAddressCertificate(cert, context); case Cardano.CertificateType.StakeDeregistration: - return getStakeAddressCertificate(cert, context, Ledger.CertificateType.STAKE_DEREGISTRATION); + return getStakeAddressCertificate(cert, context); case Cardano.CertificateType.StakeDelegation: return stakeDelegationCertificate(cert, context); case Cardano.CertificateType.PoolRegistration: return poolRegistrationCertificate(cert, context); case Cardano.CertificateType.PoolRetirement: return poolRetirementCertificate(cert, context); + + // Conway Era Certs + case Cardano.CertificateType.Registration: + return getNewStakeAddressCertificate(cert, context); + case Cardano.CertificateType.Unregistration: + return getNewStakeAddressCertificate(cert, context); + case Cardano.CertificateType.VoteDelegation: + return voteDelegationCertificate(cert, context); + case Cardano.CertificateType.RegisterDelegateRepresentative: + return await drepRegistrationCertificate(cert, context); + case Cardano.CertificateType.UnregisterDelegateRepresentative: + return await drepRegistrationCertificate(cert, context); + case Cardano.CertificateType.UpdateDelegateRepresentative: + return await updateDRepCertificate(cert, context); default: throw new InvalidArgumentError('cert', `Certificate ${cert.__typename} not supported.`); } }; -export const mapCerts = ( +export const mapCerts = async ( certs: Cardano.Certificate[] | undefined, context: LedgerTxTransformerContext -): Ledger.Certificate[] | null => { +): Promise => { if (!certs) return null; - return certs.map((coreCert) => toCert(coreCert, context)); + return Promise.all(certs.map((coreCert) => toCert(coreCert, context))); }; diff --git a/packages/hardware-ledger/src/transformers/index.ts b/packages/hardware-ledger/src/transformers/index.ts index 701ffa23c56..f4a0ed30745 100644 --- a/packages/hardware-ledger/src/transformers/index.ts +++ b/packages/hardware-ledger/src/transformers/index.ts @@ -9,3 +9,4 @@ export * from './tx'; export * from './txIn'; export * from './txOut'; export * from './withdrawals'; +export * from './votingProcedures'; diff --git a/packages/hardware-ledger/src/transformers/tx.ts b/packages/hardware-ledger/src/transformers/tx.ts index 084188943d2..8267f4653b1 100644 --- a/packages/hardware-ledger/src/transformers/tx.ts +++ b/packages/hardware-ledger/src/transformers/tx.ts @@ -16,7 +16,7 @@ import { mapWithdrawals } from './withdrawals'; export const LedgerTxTransformer: Transformer = { auxiliaryData: ({ auxiliaryDataHash }) => mapAuxiliaryData(auxiliaryDataHash), - certificates: ({ certificates }, context) => mapCerts(certificates, context!), + certificates: async ({ certificates }, context) => await mapCerts(certificates, context!), collateralInputs: ({ collaterals }, context) => mapCollateralTxIns(collaterals, context!), collateralOutput: ({ collateralReturn }, context) => mapCollateralTxOut(collateralReturn, context!), donation: ({ donation }) => donation, diff --git a/packages/hardware-ledger/src/transformers/withdrawals.ts b/packages/hardware-ledger/src/transformers/withdrawals.ts index cc53985fe13..e6a1f2671e1 100644 --- a/packages/hardware-ledger/src/transformers/withdrawals.ts +++ b/packages/hardware-ledger/src/transformers/withdrawals.ts @@ -61,7 +61,7 @@ export const toWithdrawal: Transform { describe('mapCerts', () => { it('returns null when given an undefined token map', async () => { const certs: Cardano.Certificate | undefined = undefined; - const ledgerCerts = mapCerts(certs, CONTEXT_WITHOUT_KNOWN_ADDRESSES); + const ledgerCerts = await mapCerts(certs, CONTEXT_WITHOUT_KNOWN_ADDRESSES); expect(ledgerCerts).toEqual(null); }); it('can map a script hash stake registration certificate', async () => { - const ledgerCerts = mapCerts( + const ledgerCerts = await mapCerts( [ { __typename: Cardano.CertificateType.StakeRegistration, @@ -112,7 +115,7 @@ describe('certificates', () => { { params: { stakeCredential: { - scriptHashHex: 'cb0ec2692497b458e46812c8a5bfa2931d1a2d965a99893828ec810f', + scriptHashHex: '7c16240714ea0e12b41a914f2945784ac494bb19573f0ca61a08afa8', type: Ledger.CredentialParamsType.SCRIPT_HASH } }, @@ -122,7 +125,7 @@ describe('certificates', () => { }); it('can map a stake key stake registration certificate', async () => { - const ledgerCerts = mapCerts( + const ledgerCerts = await mapCerts( [ { __typename: Cardano.CertificateType.StakeRegistration, @@ -152,7 +155,7 @@ describe('certificates', () => { }); it('can map a script hash stake deregistration certificate', async () => { - const ledgerCerts = mapCerts( + const ledgerCerts = await mapCerts( [ { __typename: Cardano.CertificateType.StakeDeregistration, @@ -166,7 +169,7 @@ describe('certificates', () => { { params: { stakeCredential: { - scriptHashHex: 'cb0ec2692497b458e46812c8a5bfa2931d1a2d965a99893828ec810f', + scriptHashHex: '7c16240714ea0e12b41a914f2945784ac494bb19573f0ca61a08afa8', type: Ledger.CredentialParamsType.SCRIPT_HASH } }, @@ -176,7 +179,7 @@ describe('certificates', () => { }); it('can map a stake key stake deregistration certificate', async () => { - const ledgerCerts = mapCerts( + const ledgerCerts = await mapCerts( [ { __typename: Cardano.CertificateType.StakeDeregistration, @@ -207,7 +210,7 @@ describe('certificates', () => { it('can map a pool registration certificate with known keys', async () => { expect( - mapCerts( + await mapCerts( [ { __typename: Cardano.CertificateType.PoolRegistration, @@ -298,7 +301,7 @@ describe('certificates', () => { it('can map a pool registration certificate with unknown keys', async () => { expect( - mapCerts( + await mapCerts( [ { __typename: Cardano.CertificateType.PoolRegistration, @@ -322,14 +325,14 @@ describe('certificates', () => { pledge: 10_000n, poolKey: { params: { - keyHashHex: 'cb0ec2692497b458e46812c8a5bfa2931d1a2d965a99893828ec810f' + keyHashHex: '7c16240714ea0e12b41a914f2945784ac494bb19573f0ca61a08afa8' }, type: Ledger.PoolKeyType.THIRD_PARTY }, poolOwners: [ { params: { - stakingKeyHashHex: 'cb0ec2692497b458e46812c8a5bfa2931d1a2d965a99893828ec810f' + stakingKeyHashHex: '7c16240714ea0e12b41a914f2945784ac494bb19573f0ca61a08afa8' }, type: Ledger.PoolOwnerType.THIRD_PARTY } @@ -358,7 +361,7 @@ describe('certificates', () => { ], rewardAccount: { params: { - rewardAccountHex: 'e1cb0ec2692497b458e46812c8a5bfa2931d1a2d965a99893828ec810f' + rewardAccountHex: 'e07c16240714ea0e12b41a914f2945784ac494bb19573f0ca61a08afa8' }, type: Ledger.PoolRewardAccountType.THIRD_PARTY }, @@ -370,7 +373,7 @@ describe('certificates', () => { }); it('throws if its given a pool retirement certificate but the signing key cant be found', async () => { - expect(() => + await expect( mapCerts( [ { @@ -381,11 +384,11 @@ describe('certificates', () => { ], CONTEXT_WITHOUT_KNOWN_ADDRESSES ) - ).toThrow("Invalid argument 'certificate': Missing key matching pool retirement certificate."); + ).rejects.toThrow("Invalid argument 'certificate': Missing key matching pool retirement certificate."); }); it('can map a stake pool retirement certificate', async () => { - const ledgerCerts = mapCerts( + const ledgerCerts = await mapCerts( [ { __typename: Cardano.CertificateType.PoolRetirement, @@ -414,7 +417,7 @@ describe('certificates', () => { }); it('can map a delegation certificate with unknown stake key', async () => { - const ledgerCerts = mapCerts( + const ledgerCerts = await mapCerts( [ { __typename: Cardano.CertificateType.StakeDelegation, @@ -430,7 +433,7 @@ describe('certificates', () => { params: { poolKeyHashHex: '153806dbcd134ddee69a8c5204e38ac80448f62342f8c23cfe4b7edf', stakeCredential: { - scriptHashHex: 'cb0ec2692497b458e46812c8a5bfa2931d1a2d965a99893828ec810f', + scriptHashHex: '7c16240714ea0e12b41a914f2945784ac494bb19573f0ca61a08afa8', type: Ledger.CredentialParamsType.SCRIPT_HASH } }, @@ -440,7 +443,7 @@ describe('certificates', () => { }); it('can map a delegation certificate with known stake key', async () => { - const ledgerCerts = mapCerts( + const ledgerCerts = await mapCerts( [ { __typename: Cardano.CertificateType.StakeDelegation, @@ -474,49 +477,557 @@ describe('certificates', () => { describe('getKnownAddress', () => { it('should return undefined immediately if context is not provided', () => { - const fakeCertificate: Cardano.StakeDelegationCertificate = { - __typename: Cardano.CertificateType.StakeDelegation, - poolId: 'pool1' as Cardano.PoolId, - stakeCredential: { - hash: 'fakehash' as Hash28ByteBase16, - type: Cardano.CredentialType.KeyHash - } + const fakeCredential: Cardano.Credential = { + hash: 'fakehash' as Hash28ByteBase16, + type: Cardano.CredentialType.KeyHash }; - const result = getKnownAddress(fakeCertificate); + const result = getKnownAddress(fakeCredential); expect(result).toBeUndefined(); }); it('should return the matching known address when context is provided', () => { const hashUsedForTest = '13cf55d175ea848b87deb3e914febd7e028e2bf6534475d52fb9c3d0' as Hash28ByteBase16; - const fakeCertificate: Cardano.StakeDelegationCertificate = { - __typename: Cardano.CertificateType.StakeDelegation, - poolId: 'pool1' as Cardano.PoolId, - stakeCredential: { - hash: hashUsedForTest, - type: Cardano.CredentialType.KeyHash - } + const fakeCredential: Cardano.Credential = { + hash: hashUsedForTest, + type: Cardano.CredentialType.KeyHash }; const expectedAddress = mockContext.knownAddresses[0]; - const result = getKnownAddress(fakeCertificate, mockContext); + const result = getKnownAddress(fakeCredential, mockContext); expect(result).not.toBeUndefined(); expect(result).toEqual(expectedAddress); }); it('should return undefined if no addresses match the stake credential hash', () => { const hashUsedForTest = 'unknown-hash' as Hash28ByteBase16; - const fakeCertificate: Cardano.StakeDelegationCertificate = { - __typename: Cardano.CertificateType.StakeDelegation, - poolId: 'pool1' as Cardano.PoolId, - stakeCredential: { - hash: hashUsedForTest, - type: Cardano.CredentialType.KeyHash - } + const fakeCredential: Cardano.Credential = { + hash: hashUsedForTest, + type: Cardano.CredentialType.KeyHash }; - const result = getKnownAddress(fakeCertificate, mockContext); + const result = getKnownAddress(fakeCredential, mockContext); expect(result).toBeUndefined(); }); }); + describe('conway-era', () => { + describe('Cardano.CertificateType.Registration', () => { + it('can map a script hash type of params', async () => { + const ledgerCerts = await mapCerts( + [ + { + __typename: Cardano.CertificateType.Registration, + deposit: 5n, + stakeCredential + } + ], + CONTEXT_WITHOUT_KNOWN_ADDRESSES + ); + + expect(ledgerCerts).toEqual([ + { + params: { + deposit: 5n, + stakeCredential: { + scriptHashHex: '7c16240714ea0e12b41a914f2945784ac494bb19573f0ca61a08afa8', + type: Ledger.CredentialParamsType.SCRIPT_HASH + } + }, + type: Ledger.CertificateType.STAKE_REGISTRATION_CONWAY + } + ]); + }); + + it('can map a key path type of params', async () => { + const ledgerCerts = await mapCerts( + [ + { + __typename: Cardano.CertificateType.Registration, + deposit: 5n, + stakeCredential + } + ], + CONTEXT_WITH_KNOWN_ADDRESSES + ); + + expect(ledgerCerts).toEqual([ + { + params: { + deposit: 5n, + stakeCredential: { + keyPath: [ + util.harden(CardanoKeyConst.PURPOSE), + util.harden(CardanoKeyConst.COIN_TYPE), + util.harden(0), + KeyRole.Stake, + 0 + ], + type: Ledger.CredentialParamsType.KEY_PATH + } + }, + type: Ledger.CertificateType.STAKE_REGISTRATION_CONWAY + } + ]); + }); + + it.todo('can map a key hash type of params'); + }); + describe('Cardano.CertificateType.Unregistration', () => { + it('can map a script hash type of params', async () => { + const ledgerCerts = await mapCerts( + [ + { + __typename: Cardano.CertificateType.Unregistration, + deposit: 5n, + stakeCredential + } + ], + CONTEXT_WITHOUT_KNOWN_ADDRESSES + ); + + expect(ledgerCerts).toEqual([ + { + params: { + deposit: 5n, + stakeCredential: { + scriptHashHex: '7c16240714ea0e12b41a914f2945784ac494bb19573f0ca61a08afa8', + type: Ledger.CredentialParamsType.SCRIPT_HASH + } + }, + type: Ledger.CertificateType.STAKE_DEREGISTRATION_CONWAY + } + ]); + }); + + it('can map a key path type of params', async () => { + const ledgerCerts = await mapCerts( + [ + { + __typename: Cardano.CertificateType.Unregistration, + deposit: 5n, + stakeCredential + } + ], + CONTEXT_WITH_KNOWN_ADDRESSES + ); + + expect(ledgerCerts).toEqual([ + { + params: { + deposit: 5n, + stakeCredential: { + keyPath: [ + util.harden(CardanoKeyConst.PURPOSE), + util.harden(CardanoKeyConst.COIN_TYPE), + util.harden(0), + KeyRole.Stake, + 0 + ], + type: Ledger.CredentialParamsType.KEY_PATH + } + }, + type: Ledger.CertificateType.STAKE_DEREGISTRATION_CONWAY + } + ]); + }); + + it.todo('can map a key hash type of params'); + }); + + describe('Cardano.CertificateType.VoteDelegation', () => { + it('can map always abstain type of drep', async () => { + const ledgerCerts = await mapCerts( + [ + { + __typename: Cardano.CertificateType.VoteDelegation, + dRep: { __typename: 'AlwaysAbstain' }, + stakeCredential + } + ], + CONTEXT_WITHOUT_KNOWN_ADDRESSES + ); + + expect(ledgerCerts).toEqual([ + { + params: { + dRep: { type: Ledger.DRepParamsType.ABSTAIN }, + stakeCredential: { + scriptHashHex: '7c16240714ea0e12b41a914f2945784ac494bb19573f0ca61a08afa8', + type: Ledger.CredentialParamsType.SCRIPT_HASH + } + }, + type: Ledger.CertificateType.VOTE_DELEGATION + } + ]); + }); + it('can map always no confidence type of drep', async () => { + const ledgerCerts = await mapCerts( + [ + { + __typename: Cardano.CertificateType.VoteDelegation, + dRep: { __typename: 'AlwaysNoConfidence' }, + stakeCredential + } + ], + CONTEXT_WITHOUT_KNOWN_ADDRESSES + ); + + expect(ledgerCerts).toEqual([ + { + params: { + dRep: { type: Ledger.DRepParamsType.NO_CONFIDENCE }, + stakeCredential: { + scriptHashHex: '7c16240714ea0e12b41a914f2945784ac494bb19573f0ca61a08afa8', + type: Ledger.CredentialParamsType.SCRIPT_HASH + } + }, + type: Ledger.CertificateType.VOTE_DELEGATION + } + ]); + }); + it('can map dRep credential type of drep', async () => { + const ledgerCerts = await mapCerts( + [ + { + __typename: Cardano.CertificateType.VoteDelegation, + dRep: stakeCredential, + stakeCredential + } + ], + CONTEXT_WITHOUT_KNOWN_ADDRESSES + ); + + expect(ledgerCerts).toEqual([ + { + params: { + dRep: { + keyPath: [ + util.harden(CardanoKeyConst.PURPOSE), + util.harden(CardanoKeyConst.COIN_TYPE), + util.harden(0), + 3, + 0 + ], + type: Ledger.DRepParamsType.KEY_PATH + }, + stakeCredential: { + scriptHashHex: '7c16240714ea0e12b41a914f2945784ac494bb19573f0ca61a08afa8', + type: Ledger.CredentialParamsType.SCRIPT_HASH + } + }, + type: Ledger.CertificateType.VOTE_DELEGATION + } + ]); + }); + }); + + describe('Cardano.CertificateType.RegisterDelegateRepresentative', () => { + it('can map a key path type of params', async () => { + const ledgerCerts = await mapCerts( + [ + { + __typename: Cardano.CertificateType.RegisterDelegateRepresentative, + anchor: { + dataHash: metadataJson.hash, + url: metadataJson.url + }, + dRepCredential, + deposit: 5n + } + ], + CONTEXT_WITH_KNOWN_ADDRESSES + ); + + expect(ledgerCerts).toEqual([ + { + params: { + anchor: { hashHex: metadataJson.hash, url: metadataJson.url }, + dRepCredential: { + keyPath: [2_147_485_500, 2_147_485_463, 2_147_483_648, 3, 0], + type: Ledger.CredentialParamsType.KEY_PATH + }, + deposit: 5n + }, + type: Ledger.CertificateType.DREP_REGISTRATION + } + ]); + }); + + it('can map a script hash type of params', async () => { + const ledgerCerts = await mapCerts( + [ + { + __typename: Cardano.CertificateType.RegisterDelegateRepresentative, + anchor: { + dataHash: metadataJson.hash, + url: metadataJson.url + }, + dRepCredential: { + ...dRepCredential, + type: Cardano.CredentialType.ScriptHash + }, + deposit: 5n + } + ], + CONTEXT_WITH_KNOWN_ADDRESSES + ); + + expect(ledgerCerts).toEqual([ + { + params: { + anchor: { hashHex: metadataJson.hash, url: metadataJson.url }, + dRepCredential: { + scriptHashHex: 'b276b4f7a706a81364de606d890343a76af570268d4bbfee2fc8fcab', + type: Ledger.CredentialParamsType.SCRIPT_HASH + }, + deposit: 5n + }, + type: Ledger.CertificateType.DREP_REGISTRATION + } + ]); + }); + + it("it throws if public key doesn't match credential", async () => { + await expect( + mapCerts( + [ + { + __typename: Cardano.CertificateType.RegisterDelegateRepresentative, + anchor: { + dataHash: metadataJson.hash, + url: metadataJson.url + }, + dRepCredential: { + ...dRepCredential, + type: Cardano.CredentialType.ScriptHash + }, + deposit: 5n + } + ], + CONTEXT_WITHOUT_KNOWN_ADDRESSES + ) + ).rejects.toThrowError('dRepPublicKey does not match certificate drep credential.'); + }); + + it("it throws if public key wasn't provided", async () => { + await expect( + mapCerts( + [ + { + __typename: Cardano.CertificateType.RegisterDelegateRepresentative, + anchor: { + dataHash: metadataJson.hash, + url: metadataJson.url + }, + dRepCredential: { + ...dRepCredential, + type: Cardano.CredentialType.ScriptHash + }, + deposit: 5n + } + ], + { ...CONTEXT_WITHOUT_KNOWN_ADDRESSES, dRepPublicKey: undefined } + ) + ).rejects.toThrowError('dRepPublicKey does not match certificate drep credential.'); + }); + }); + + describe('Cardano.CertificateType.UnregisterDelegateRepresentative', () => { + it('can map a key path type of params', async () => { + const ledgerCerts = await mapCerts( + [ + { + __typename: Cardano.CertificateType.UnregisterDelegateRepresentative, + dRepCredential, + deposit: 5n + } + ], + CONTEXT_WITH_KNOWN_ADDRESSES + ); + + expect(ledgerCerts).toEqual([ + { + params: { + dRepCredential: { + keyPath: [2_147_485_500, 2_147_485_463, 2_147_483_648, 3, 0], + type: Ledger.CredentialParamsType.KEY_PATH + }, + deposit: 5n + }, + type: Ledger.CertificateType.DREP_DEREGISTRATION + } + ]); + }); + + it('can map a script hash type of params', async () => { + const ledgerCerts = await mapCerts( + [ + { + __typename: Cardano.CertificateType.UnregisterDelegateRepresentative, + dRepCredential: { + ...dRepCredential, + type: Cardano.CredentialType.ScriptHash + }, + deposit: 5n + } + ], + CONTEXT_WITH_KNOWN_ADDRESSES + ); + + expect(ledgerCerts).toEqual([ + { + params: { + dRepCredential: { + scriptHashHex: 'b276b4f7a706a81364de606d890343a76af570268d4bbfee2fc8fcab', + type: Ledger.CredentialParamsType.SCRIPT_HASH + }, + deposit: 5n + }, + type: Ledger.CertificateType.DREP_DEREGISTRATION + } + ]); + }); + + it("it throws if public key doesn't match credential", async () => { + await expect( + mapCerts( + [ + { + __typename: Cardano.CertificateType.UnregisterDelegateRepresentative, + dRepCredential: { + ...dRepCredential, + type: Cardano.CredentialType.ScriptHash + }, + deposit: 5n + } + ], + CONTEXT_WITHOUT_KNOWN_ADDRESSES + ) + ).rejects.toThrowError('dRepPublicKey does not match certificate drep credential.'); + }); + + it("it throws if public key wasn't provided", async () => { + await expect( + mapCerts( + [ + { + __typename: Cardano.CertificateType.UnregisterDelegateRepresentative, + dRepCredential: { + ...dRepCredential, + type: Cardano.CredentialType.ScriptHash + }, + deposit: 5n + } + ], + { ...CONTEXT_WITHOUT_KNOWN_ADDRESSES, dRepPublicKey: undefined } + ) + ).rejects.toThrowError('dRepPublicKey does not match certificate drep credential.'); + }); + }); + + describe('Cardano.CertificateType.UpdateDelegateRepresentative', () => { + it('can map a key path type of params', async () => { + const ledgerCerts = await mapCerts( + [ + { + __typename: Cardano.CertificateType.UpdateDelegateRepresentative, + anchor: { + dataHash: metadataJson.hash, + url: metadataJson.url + }, + dRepCredential + } + ], + CONTEXT_WITH_KNOWN_ADDRESSES + ); + + expect(ledgerCerts).toEqual([ + { + params: { + anchor: { hashHex: metadataJson.hash, url: metadataJson.url }, + dRepCredential: { + keyPath: [2_147_485_500, 2_147_485_463, 2_147_483_648, 3, 0], + type: Ledger.CredentialParamsType.KEY_PATH + } + }, + type: Ledger.CertificateType.DREP_UPDATE + } + ]); + }); + + it('can map a script hash type of params', async () => { + const ledgerCerts = await mapCerts( + [ + { + __typename: Cardano.CertificateType.UpdateDelegateRepresentative, + anchor: { + dataHash: metadataJson.hash, + url: metadataJson.url + }, + dRepCredential: { + ...dRepCredential, + type: Cardano.CredentialType.ScriptHash + } + } + ], + CONTEXT_WITH_KNOWN_ADDRESSES + ); + + expect(ledgerCerts).toEqual([ + { + params: { + anchor: { hashHex: metadataJson.hash, url: metadataJson.url }, + dRepCredential: { + scriptHashHex: 'b276b4f7a706a81364de606d890343a76af570268d4bbfee2fc8fcab', + type: Ledger.CredentialParamsType.SCRIPT_HASH + } + }, + type: Ledger.CertificateType.DREP_UPDATE + } + ]); + }); + + it("it throws if public key doesn't match credential", async () => { + await expect( + mapCerts( + [ + { + __typename: Cardano.CertificateType.UpdateDelegateRepresentative, + anchor: { + dataHash: metadataJson.hash, + url: metadataJson.url + }, + dRepCredential: { + ...dRepCredential, + type: Cardano.CredentialType.ScriptHash + } + } + ], + CONTEXT_WITHOUT_KNOWN_ADDRESSES + ) + ).rejects.toThrowError('dRepPublicKey does not match certificate drep credential.'); + }); + + it("it throws if public key wasn't provided", async () => { + await expect( + mapCerts( + [ + { + __typename: Cardano.CertificateType.UpdateDelegateRepresentative, + anchor: { + dataHash: metadataJson.hash, + url: metadataJson.url + }, + dRepCredential: { + ...dRepCredential, + type: Cardano.CredentialType.ScriptHash + } + } + ], + { ...CONTEXT_WITHOUT_KNOWN_ADDRESSES, dRepPublicKey: undefined } + ) + ).rejects.toThrowError('dRepPublicKey does not match certificate drep credential.'); + }); + }); + }); }); diff --git a/packages/hardware-ledger/test/transformers/tx.test.ts b/packages/hardware-ledger/test/transformers/tx.test.ts index 7023611018e..9cdca7e31ad 100644 --- a/packages/hardware-ledger/test/transformers/tx.test.ts +++ b/packages/hardware-ledger/test/transformers/tx.test.ts @@ -1,5 +1,6 @@ import * as Ledger from '@cardano-foundation/ledgerjs-hw-app-cardano'; -import { CONTEXT_WITH_KNOWN_ADDRESSES, babbageTxWithoutScript, tx } from '../testData'; +import { CONTEXT_WITH_KNOWN_ADDRESSES, babbageTxWithoutScript, stakeCredential, tx } from '../testData'; +import { Cardano } from '@cardano-sdk/core'; import { CardanoKeyConst, TxInId, util } from '@cardano-sdk/key-management'; import { toLedgerTx } from '../../src'; @@ -250,5 +251,171 @@ describe('tx', () => { withdrawals: null }); }); + + test('can map a transaction with new conway-era certs', async () => { + const stakeKeyPath = { index: 0, role: 2 }; + const txBodyWithRegistrationCert = { + ...tx.body, + certificates: [ + { + __typename: Cardano.CertificateType.Registration, + deposit: 5n, + stakeCredential + } as Cardano.Certificate + ] + }; + + expect( + await toLedgerTx(txBodyWithRegistrationCert, { + ...CONTEXT_WITH_KNOWN_ADDRESSES + }) + ).toEqual({ + auxiliaryData: { + params: { + hashHex: '2ceb364d93225b4a0f004a0975a13eb50c3cc6348474b4fe9121f8dc72ca0cfa' + }, + type: Ledger.TxAuxiliaryDataType.ARBITRARY_HASH + }, + certificates: [ + { + params: { + deposit: 5n, + stakeCredential: { + keyPath: [ + util.harden(CardanoKeyConst.PURPOSE), + util.harden(CardanoKeyConst.COIN_TYPE), + util.harden(CONTEXT_WITH_KNOWN_ADDRESSES.accountIndex), + stakeKeyPath.role, + stakeKeyPath.index + ], + type: 0 + } + }, + type: Ledger.CertificateType.STAKE_REGISTRATION_CONWAY + } + ], + collateralInputs: [ + { + outputIndex: 1, + path: null, + txHashHex: '0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5' + } + ], + collateralOutput: null, + fee: 10n, + includeNetworkId: false, + inputs: [ + { + outputIndex: 0, + path: null, + txHashHex: '0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5' + } + ], + mint: [ + { + policyIdHex: '2a286ad895d091f2b3d168a6091ad2627d30a72761a5bc36eef00740', + tokens: [ + { + amount: 20n, + assetNameHex: '' + } + ] + }, + { + policyIdHex: '659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba82', + tokens: [ + { + amount: -50n, + assetNameHex: '54534c41' + } + ] + }, + { + policyIdHex: '7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc373', + tokens: [ + { + amount: 40n, + assetNameHex: '' + }, + { + amount: 30n, + assetNameHex: '504154415445' + } + ] + } + ], + network: { + networkId: Ledger.Networks.Testnet.networkId, + protocolMagic: 999 + }, + outputs: [ + { + amount: 10n, + datumHashHex: '0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5', + destination: { + params: { + addressHex: + '009493315cd92eb5d8c4304e67b7e16ae36d61d34502694657811a2c8e32c728d3861e164cab28cb8f006448139c8f1740ffb8e7aa9e5232dc' + }, + type: Ledger.TxOutputDestinationType.THIRD_PARTY + }, + format: Ledger.TxOutputFormat.ARRAY_LEGACY, + tokenBundle: [ + { + policyIdHex: '2a286ad895d091f2b3d168a6091ad2627d30a72761a5bc36eef00740', + tokens: [ + { + amount: 20n, + assetNameHex: '' + } + ] + }, + { + policyIdHex: '659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba82', + tokens: [ + { + amount: 50n, + assetNameHex: '54534c41' + } + ] + }, + { + policyIdHex: '7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc373', + tokens: [ + { + amount: 40n, + assetNameHex: '' + }, + { + amount: 30n, + assetNameHex: '504154415445' + } + ] + } + ] + } + ], + referenceInputs: null, + requiredSigners: [ + { + hashHex: '6199186adb51974690d7247d2646097d2c62763b16fb7ed3f9f55d39', + type: Ledger.TxRequiredSignerType.HASH + } + ], + scriptDataHashHex: '6199186adb51974690d7247d2646097d2c62763b16fb7ed3f9f55d38abc123de', + ttl: 1000, + validityIntervalStart: 100, + votingProcedures: null, + withdrawals: [ + { + amount: 5n, + stakeCredential: { + keyHashHex: '13cf55d175ea848b87deb3e914febd7e028e2bf6534475d52fb9c3d0', + type: Ledger.CredentialParamsType.KEY_HASH + } + } + ] + }); + }); }); }); diff --git a/packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts b/packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts index ce4989b3b01..f3e21298ec1 100644 --- a/packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts +++ b/packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts @@ -19,7 +19,7 @@ import { firstValueFrom } from 'rxjs'; import { getDevices } from '@ledgerhq/hw-transport-node-hid-noevents'; import { dummyLogger as logger } from 'ts-log'; import { mockKeyAgentDependencies } from '../../../../key-management/test/mocks'; -import DeviceConnection from '@cardano-foundation/ledgerjs-hw-app-cardano'; +import DeviceConnection, { InvalidDataReason } from '@cardano-foundation/ledgerjs-hw-app-cardano'; const getHidDevice = () => { const ledgerDevicePath = getDevices()[0]?.path; @@ -37,6 +37,14 @@ const cleanupEstablishedConnections = async () => { LedgerKeyAgent.deviceConnections = []; }; +const getStakeCredential = (rewardAccount: Cardano.RewardAccount) => { + const stakeKeyHash = Cardano.RewardAccount.toHash(rewardAccount); + return { + hash: Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(stakeKeyHash), + type: Cardano.CredentialType.KeyHash + }; +}; + describe('LedgerKeyAgent', () => { describe('general', () => { afterEach(cleanupEstablishedConnections); @@ -180,6 +188,14 @@ describe('LedgerKeyAgent', () => { afterAll(() => wallet.shutdown()); + let dRepPublicKey: Crypto.Ed25519PublicKeyHex; + let dRepKeyHash: Crypto.Ed25519KeyHashHex; + + beforeEach(async () => { + dRepPublicKey = Crypto.Ed25519PublicKeyHex('b3691d42417d8307ad71da8586c2b439965545f481343b9073324ae60ad263f6'); + dRepKeyHash = (await Crypto.Ed25519PublicKey.fromHex(dRepPublicKey).hash()).hex(); + }); + it('successfully signs a transaction with assets and validity interval', async () => { const { witness: { signatures } @@ -298,6 +314,316 @@ describe('LedgerKeyAgent', () => { } = await wallet.finalizeTx({ tx: unsignedTx }); expect(signatures.size).toBe(2); }); + + describe('conway-era', () => { + describe('ordinary tx mode', () => { + it('can sign a transaction with Registration certs', async () => { + const txBuilder = wallet.createTxBuilder(); + txBuilder.partialTxBody.certificates = [ + { + __typename: Cardano.CertificateType.Registration, + deposit: 5n, + stakeCredential: getStakeCredential( + (await firstValueFrom(wallet.delegation.rewardAccounts$))?.[0].address + ) + } + ]; + const tx = txBuilder + .addOutput( + txBuilder.buildOutput().address(outputs[0].address).coin(BigInt(outputs[0].value.coins)).toTxOut() + ) + .build(); + + expect(await tx.sign()).toBeTruthy(); + }); + + it('would throw while trying to sign a transaction with Registration certs', async () => { + const txBuilder = wallet.createTxBuilder(); + txBuilder.partialTxBody.certificates = [ + { + __typename: Cardano.CertificateType.Registration, + deposit: 5n, + stakeCredential: getStakeCredential( + Cardano.RewardAccount('stake_test1up7pvfq8zn4quy45r2g572290p9vf99mr9tn7r9xrgy2l2qdsf58d') + ) + } + ]; + const tx = txBuilder + .addOutput( + txBuilder.buildOutput().address(outputs[0].address).coin(BigInt(outputs[0].value.coins)).toTxOut() + ) + .build(); + + await expect(tx.sign()).rejects.toThrow( + InvalidDataReason.SIGN_MODE_ORDINARY__CERTIFICATE_STAKE_CREDENTIAL_ONLY_AS_PATH + ); + }); + + it('can sign a transaction with Unregistration certs', async () => { + const txBuilder = wallet.createTxBuilder(); + txBuilder.partialTxBody.certificates = [ + { + __typename: Cardano.CertificateType.Unregistration, + deposit: 5n, + stakeCredential: getStakeCredential( + (await firstValueFrom(wallet.delegation.rewardAccounts$))?.[0].address + ) + } + ]; + const tx = txBuilder + .addOutput( + txBuilder.buildOutput().address(outputs[0].address).coin(BigInt(outputs[0].value.coins)).toTxOut() + ) + .build(); + + expect(await tx.sign()).toBeTruthy(); + }); + + it('would throw while trying to sign a transaction with Unregistration certs', async () => { + const txBuilder = wallet.createTxBuilder(); + txBuilder.partialTxBody.certificates = [ + { + __typename: Cardano.CertificateType.Unregistration, + deposit: 5n, + stakeCredential: getStakeCredential( + Cardano.RewardAccount('stake_test1up7pvfq8zn4quy45r2g572290p9vf99mr9tn7r9xrgy2l2qdsf58d') + ) + } + ]; + const tx = txBuilder + .addOutput( + txBuilder.buildOutput().address(outputs[0].address).coin(BigInt(outputs[0].value.coins)).toTxOut() + ) + .build(); + + await expect(tx.sign()).rejects.toThrow( + InvalidDataReason.SIGN_MODE_ORDINARY__CERTIFICATE_STAKE_CREDENTIAL_ONLY_AS_PATH + ); + }); + + it('can sign a transaction with VoteDelegation certs with dRep of credential type', async () => { + const txBuilder = wallet.createTxBuilder(); + txBuilder.partialTxBody.certificates = [ + { + __typename: Cardano.CertificateType.VoteDelegation, + dRep: { + hash: Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(dRepKeyHash), + type: Cardano.CredentialType.KeyHash + }, + stakeCredential: getStakeCredential( + (await firstValueFrom(wallet.delegation.rewardAccounts$))?.[0].address + ) + } + ]; + const tx = txBuilder + .addOutput( + txBuilder.buildOutput().address(outputs[0].address).coin(BigInt(outputs[0].value.coins)).toTxOut() + ) + .build(); + + expect(await tx.sign()).toBeTruthy(); + }); + + it('can sign a transaction with VoteDelegation certs with dRep of AlwaysAbstain type', async () => { + const txBuilder = wallet.createTxBuilder(); + txBuilder.partialTxBody.certificates = [ + { + __typename: Cardano.CertificateType.VoteDelegation, + dRep: { __typename: 'AlwaysAbstain' }, + stakeCredential: getStakeCredential( + (await firstValueFrom(wallet.delegation.rewardAccounts$))?.[0].address + ) + } + ]; + const tx = txBuilder + .addOutput( + txBuilder.buildOutput().address(outputs[0].address).coin(BigInt(outputs[0].value.coins)).toTxOut() + ) + .build(); + + expect(await tx.sign()).toBeTruthy(); + }); + + it('can sign a transaction with VoteDelegation certs with dRep of AlwaysNoConfidence type', async () => { + const txBuilder = wallet.createTxBuilder(); + txBuilder.partialTxBody.certificates = [ + { + __typename: Cardano.CertificateType.VoteDelegation, + dRep: { __typename: 'AlwaysNoConfidence' }, + stakeCredential: getStakeCredential( + (await firstValueFrom(wallet.delegation.rewardAccounts$))?.[0].address + ) + } + ]; + const tx = txBuilder + .addOutput( + txBuilder.buildOutput().address(outputs[0].address).coin(BigInt(outputs[0].value.coins)).toTxOut() + ) + .build(); + + expect(await tx.sign()).toBeTruthy(); + }); + + it('would throw while trying to sign a transaction with VoteDelegation certs', async () => { + const txBuilder = wallet.createTxBuilder(); + txBuilder.partialTxBody.certificates = [ + { + __typename: Cardano.CertificateType.VoteDelegation, + dRep: { + hash: Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(dRepKeyHash), + type: Cardano.CredentialType.KeyHash + }, + stakeCredential: getStakeCredential( + Cardano.RewardAccount('stake_test1up7pvfq8zn4quy45r2g572290p9vf99mr9tn7r9xrgy2l2qdsf58d') + ) + } + ]; + const tx = txBuilder + .addOutput( + txBuilder.buildOutput().address(outputs[0].address).coin(BigInt(outputs[0].value.coins)).toTxOut() + ) + .build(); + + await expect(tx.sign()).rejects.toThrow( + InvalidDataReason.SIGN_MODE_ORDINARY__CERTIFICATE_STAKE_CREDENTIAL_ONLY_AS_PATH + ); + }); + + it('can sign a transaction with RegisterDelegateRepresentative certs', async () => { + const txBuilder = wallet.createTxBuilder(); + txBuilder.partialTxBody.certificates = [ + { + __typename: Cardano.CertificateType.RegisterDelegateRepresentative, + anchor: null, + dRepCredential: { + hash: Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(dRepKeyHash), + type: Cardano.CredentialType.KeyHash + }, + deposit: 5n + } + ]; + const tx = txBuilder + .addOutput( + txBuilder.buildOutput().address(outputs[0].address).coin(BigInt(outputs[0].value.coins)).toTxOut() + ) + .build(); + + expect(await tx.sign()).toBeTruthy(); + }); + + it('would throw while trying to sign a transaction with RegisterDelegateRepresentative certs', async () => { + const txBuilder = wallet.createTxBuilder(); + txBuilder.partialTxBody.certificates = [ + { + __typename: Cardano.CertificateType.RegisterDelegateRepresentative, + anchor: null, + dRepCredential: { + hash: Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(dRepKeyHash), + type: Cardano.CredentialType.ScriptHash + }, + deposit: 5n + } + ]; + const tx = txBuilder + .addOutput( + txBuilder.buildOutput().address(outputs[0].address).coin(BigInt(outputs[0].value.coins)).toTxOut() + ) + .build(); + + await expect(tx.sign()).rejects.toThrow( + InvalidDataReason.SIGN_MODE_ORDINARY__CERTIFICATE_DREP_CREDENTIAL_ONLY_AS_PATH + ); + }); + + it('can sign a transaction with UnregisterDelegateRepresentative certs', async () => { + const txBuilder = wallet.createTxBuilder(); + txBuilder.partialTxBody.certificates = [ + { + __typename: Cardano.CertificateType.UnregisterDelegateRepresentative, + dRepCredential: { + hash: Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(dRepKeyHash), + type: Cardano.CredentialType.KeyHash + }, + deposit: 5n + } + ]; + const tx = txBuilder + .addOutput( + txBuilder.buildOutput().address(outputs[0].address).coin(BigInt(outputs[0].value.coins)).toTxOut() + ) + .build(); + + expect(await tx.sign()).toBeTruthy(); + }); + + it('would throw while trying to sign a transaction with UnregisterDelegateRepresentative certs', async () => { + const txBuilder = wallet.createTxBuilder(); + txBuilder.partialTxBody.certificates = [ + { + __typename: Cardano.CertificateType.UnregisterDelegateRepresentative, + dRepCredential: { + hash: Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(dRepKeyHash), + type: Cardano.CredentialType.ScriptHash + }, + deposit: 5n + } + ]; + const tx = txBuilder + .addOutput( + txBuilder.buildOutput().address(outputs[0].address).coin(BigInt(outputs[0].value.coins)).toTxOut() + ) + .build(); + + await expect(tx.sign()).rejects.toThrow( + InvalidDataReason.SIGN_MODE_ORDINARY__CERTIFICATE_DREP_CREDENTIAL_ONLY_AS_PATH + ); + }); + + it('can sign a transaction with UpdateDelegateRepresentative certs', async () => { + const txBuilder = wallet.createTxBuilder(); + txBuilder.partialTxBody.certificates = [ + { + __typename: Cardano.CertificateType.UpdateDelegateRepresentative, + anchor: null, + dRepCredential: { + hash: Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(dRepKeyHash), + type: Cardano.CredentialType.KeyHash + } + } + ]; + const tx = txBuilder + .addOutput( + txBuilder.buildOutput().address(outputs[0].address).coin(BigInt(outputs[0].value.coins)).toTxOut() + ) + .build(); + + expect(await tx.sign()).toBeTruthy(); + }); + + it('would throw while trying to sign a transaction with UpdateDelegateRepresentative certs', async () => { + const txBuilder = wallet.createTxBuilder(); + txBuilder.partialTxBody.certificates = [ + { + __typename: Cardano.CertificateType.UpdateDelegateRepresentative, + anchor: null, + dRepCredential: { + hash: Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(dRepKeyHash), + type: Cardano.CredentialType.ScriptHash + } + } + ]; + const tx = txBuilder + .addOutput( + txBuilder.buildOutput().address(outputs[0].address).coin(BigInt(outputs[0].value.coins)).toTxOut() + ) + .build(); + + await expect(tx.sign()).rejects.toThrow( + InvalidDataReason.SIGN_MODE_ORDINARY__CERTIFICATE_DREP_CREDENTIAL_ONLY_AS_PATH + ); + }); + }); + }); }); describe('serializableData', () => {