From 80587843b1cbccf2ad65657351860f44673cbfb3 Mon Sep 17 00:00:00 2001 From: slowbackspace Date: Tue, 8 Oct 2024 16:23:38 +0200 Subject: [PATCH] feat: voting delegation --- .vscode/settings.json | 2 +- CHANGELOG.MD | 8 ++ docs/badge-coverage.svg | 2 +- src/constants/index.ts | 3 + src/types/types.ts | 28 +++++- src/utils/common.ts | 30 +++++++ src/utils/trezor/index.ts | 2 + src/utils/trezor/transformations.ts | 37 +++++++- tests/methods/fixtures/largestFirst.ts | 87 ++++++++++++++++++- tests/setup.ts | 2 +- .../utils/trezor/fixtures/transformations.ts | 19 ++++ tests/utils/trezor/transformations.test.ts | 6 ++ 12 files changed, 220 insertions(+), 6 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 48cd83e..ef727d1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,5 @@ }, "typescript.tsdk": ".yarn/sdks/typescript/lib", "typescript.enablePromptUseWorkspaceTsdk": true, - "cSpell.words": ["preprod"] + "cSpell.words": ["DEREGISTRATION", "drep", "preprod"] } diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 980a339..c277757 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Support for voting delegation certificates + +### Changed + +- Upgraded cardano-serialization-lib to Chang-compatible release (v12) + ## [2.2.1] - 2023-11-29 ### Fixed diff --git a/docs/badge-coverage.svg b/docs/badge-coverage.svg index 6d18e4a..986ac1f 100644 --- a/docs/badge-coverage.svg +++ b/docs/badge-coverage.svg @@ -1 +1 @@ -Coverage: 85.47%Coverage85.47% \ No newline at end of file +Coverage: 87%Coverage87% \ No newline at end of file diff --git a/src/constants/index.ts b/src/constants/index.ts index 228e3a0..130e322 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -5,6 +5,9 @@ export const CertificateType = { STAKE_DEREGISTRATION: 1, STAKE_DELEGATION: 2, STAKE_POOL_REGISTRATION: 3, + STAKE_REGISTRATION_CONWAY: 7, + STAKE_DEREGISTRATION_CONWAY: 8, + VOTE_DELEGATION: 9, } as const; export const ERROR = { diff --git a/src/types/types.ts b/src/types/types.ts index 7977da2..80b4184 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -67,6 +67,23 @@ export enum CardanoAddressType { REWARD_SCRIPT = 15, } +export enum CardanoDRepType { + KEY_HASH = 0, + SCRIPT_HASH = 1, + ABSTAIN = 2, + NO_CONFIDENCE = 3, +} + +export type DRep = + | { + type: CardanoDRepType.KEY_HASH | CardanoDRepType.SCRIPT_HASH; + keyHash: string; + } + | { + type: CardanoDRepType.ABSTAIN | CardanoDRepType.NO_CONFIDENCE; + keyHash?: never; + }; + export interface CoinSelectionResult { tx: { body: string; hash: string; size: number }; inputs: Utxo[]; @@ -116,10 +133,19 @@ export interface CertificateStakePoolRegistration { pool_parameters: Record; } +export interface CertificateVoteDelegation { + type: CertificateTypeType['VOTE_DELEGATION']; + dRep: { + type: CardanoDRepType; + keyHash: string; + }; +} + export type Certificate = | CertificateStakeRegistration | CertificateStakeDelegation - | CertificateStakePoolRegistration; + | CertificateStakePoolRegistration + | CertificateVoteDelegation; export interface Options { feeParams?: { a: string }; diff --git a/src/utils/common.ts b/src/utils/common.ts index 8def620..7e19fe3 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -15,6 +15,7 @@ import { UserOutput, Asset, ChangeOutput, + CardanoDRepType, } from '../types/types'; import { CoinSelectionError } from './errors'; @@ -299,6 +300,34 @@ export const prepareCertificates = ( CardanoWasm.StakeDeregistration.new(stakeCred), ), ); + } else if (cert.type === CertificateType.VOTE_DELEGATION) { + let targetDRep: CardanoWasm.DRep; + switch (cert.dRep.type) { + case CardanoDRepType.ABSTAIN: + targetDRep = CardanoWasm.DRep.new_always_abstain(); + break; + case CardanoDRepType.NO_CONFIDENCE: + targetDRep = CardanoWasm.DRep.new_always_no_confidence(); + break; + case CardanoDRepType.KEY_HASH: + targetDRep = CardanoWasm.DRep.new_key_hash( + CardanoWasm.Ed25519KeyHash.from_hex(cert.dRep.keyHash), + ); + break; + case CardanoDRepType.SCRIPT_HASH: + targetDRep = CardanoWasm.DRep.new_script_hash( + CardanoWasm.ScriptHash.from_hex(cert.dRep.keyHash), + ); + break; + } + + if (targetDRep) { + preparedCertificates.add( + CardanoWasm.Certificate.new_vote_delegation( + CardanoWasm.VoteDelegation.new(stakeCred, targetDRep), + ), + ); + } } else { throw new CoinSelectionError(ERROR.UNSUPPORTED_CERTIFICATE_TYPE); } @@ -311,6 +340,7 @@ export const calculateRequiredDeposit = ( ): number => { const CertificateDeposit = { [CertificateType.STAKE_DELEGATION]: 0, + [CertificateType.VOTE_DELEGATION]: 0, [CertificateType.STAKE_POOL_REGISTRATION]: 500000000, [CertificateType.STAKE_REGISTRATION]: 2000000, [CertificateType.STAKE_DEREGISTRATION]: -2000000, diff --git a/src/utils/trezor/index.ts b/src/utils/trezor/index.ts index f0ac253..b95a729 100644 --- a/src/utils/trezor/index.ts +++ b/src/utils/trezor/index.ts @@ -2,6 +2,7 @@ import { transformToTokenBundle, transformToTrezorInputs, transformToTrezorOutputs, + drepIdToHex, } from './transformations'; import { signTransaction } from './sign'; @@ -10,4 +11,5 @@ export { transformToTrezorInputs, transformToTrezorOutputs, signTransaction, + drepIdToHex, }; diff --git a/src/utils/trezor/transformations.ts b/src/utils/trezor/transformations.ts index 47c6096..d3838b9 100644 --- a/src/utils/trezor/transformations.ts +++ b/src/utils/trezor/transformations.ts @@ -1,9 +1,11 @@ +import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs'; + import { CardanoAddressParameters, CardanoInput, CardanoOutput, } from '../../types/trezor'; -import { Asset, FinalOutput, Utxo } from '../../types/types'; +import { Asset, CardanoDRepType, FinalOutput, Utxo } from '../../types/types'; import { parseAsset } from '../common'; interface AssetInPolicy { @@ -92,3 +94,36 @@ export const transformToTrezorOutputs = ( }; }); }; + +export const drepIdToHex = ( + drepId: string, +): { + type: CardanoDRepType.KEY_HASH | CardanoDRepType.SCRIPT_HASH; + hex: string; +} => { + const drep = CardanoWasm.DRep.from_bech32(drepId); + const kind = drep.kind() as unknown as + | CardanoDRepType.KEY_HASH + | CardanoDRepType.SCRIPT_HASH; + + let drepHex: string | undefined; + switch (kind) { + case CardanoDRepType.KEY_HASH: + drepHex = drep.to_key_hash()?.to_hex(); + break; + case CardanoDRepType.SCRIPT_HASH: + drepHex = drep.to_script_hash()?.to_hex(); + break; + } + + if (!drepHex) { + throw Error('Invalid drepId'); + } + + const drepData = { + type: kind, + hex: drepHex, + }; + drep.free(); + return drepData; +}; diff --git a/tests/methods/fixtures/largestFirst.ts b/tests/methods/fixtures/largestFirst.ts index 26fa547..89dcbda 100644 --- a/tests/methods/fixtures/largestFirst.ts +++ b/tests/methods/fixtures/largestFirst.ts @@ -1,4 +1,8 @@ -import { Certificate } from '../../../src/types/types'; +import { + CardanoDRepType, + Certificate, + CertificateVoteDelegation, +} from '../../../src/types/types'; import { changeAddress, setMaxAdaInputs, @@ -737,6 +741,87 @@ export const coinSelection = [ ], }, }, + { + description: + 'withdrawing rewards: 1 ADA only utxo, 1 change output, vote delegation abstain', + utxos: [utxo1], + outputs: [], + changeAddress: changeAddress, + certificates: [ + { + type: 9, + dRep: { + type: 2, // abstain + }, + } as CertificateVoteDelegation, + ], + withdrawals: [ + { + amount: '10000000', + stakingPath: "m/1852'/1815'/0'/2/0", + stakeAddress: + 'stake1u8yk3dcuj8yylwvnzz953yups6mmuvt0vtjmxl2gmgceqjqz2yfd2', + }, + ], + accountPubKey: + 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', + options: {}, + result: { + totalSpent: '177425', + fee: '177425', + deposit: '0', + inputs: [utxo1], + outputs: [ + { + isChange: true, + address: changeAddress, + amount: '14822575', + assets: [], + }, + ], + }, + }, + { + description: + 'withdrawing rewards: 1 ADA only utxo, 1 change output, vote delegation drep id', + utxos: [utxo1], + outputs: [], + changeAddress: changeAddress, + certificates: [ + { + type: 9, + dRep: { + type: CardanoDRepType.KEY_HASH, + keyHash: '3a7f09d3df4cf66a7399c2b05bfa234d5a29560c311fc5db4c490711', + }, + } as CertificateVoteDelegation, + ], + withdrawals: [ + { + amount: '10000000', + stakingPath: "m/1852'/1815'/0'/2/0", + stakeAddress: + 'stake1u8yk3dcuj8yylwvnzz953yups6mmuvt0vtjmxl2gmgceqjqz2yfd2', + }, + ], + accountPubKey: + 'ec8fdf616242f430855ad7477acda53395eb30c295f5a7ef038712578877375b5a2f00353c9c5cc88c7ff18e71dc08724d90fc238213b789c0b02438e336be07', + options: {}, + result: { + totalSpent: '178745', + fee: '178745', + deposit: '0', + inputs: [utxo1], + outputs: [ + { + isChange: true, + address: changeAddress, + amount: '14821255', + assets: [], + }, + ], + }, + }, { description: 'withdrawing rewards: multiple utxos, multiple withdrawals, 1 change output', diff --git a/tests/setup.ts b/tests/setup.ts index a5a05e3..dfad7d0 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -13,7 +13,7 @@ export const sanityCheck = (res: CoinSelectionResult): void => { (acc, input) => acc.checked_add( BigNum.from_str( - input.amount.find(a => a.unit === 'lovelace').quantity || '0', + input.amount.find(a => a.unit === 'lovelace')?.quantity || '0', ), ), BigNum.from_str('0'), diff --git a/tests/utils/trezor/fixtures/transformations.ts b/tests/utils/trezor/fixtures/transformations.ts index 6ae3a25..c8f3852 100644 --- a/tests/utils/trezor/fixtures/transformations.ts +++ b/tests/utils/trezor/fixtures/transformations.ts @@ -87,3 +87,22 @@ export const transformToTrezorOutputs = [ ], }, ]; + +export const drepIdToHex = [ + { + description: 'keyHash drep', + drepId: 'drep16pxnn38ykshfahwmkaqmke3kdqaksg4w935d7uztvh8y5l48pxv', + result: { + type: 0, + hex: 'd04d39c4e4b42e9edddbb741bb6636683b6822ae2c68df704b65ce4a', + }, + }, + { + description: 'scriptHash drep', + drepId: 'drep_script16pxnn38ykshfahwmkaqmke3kdqaksg4w935d7uztvh8y5sh6f6d', + result: { + type: 1, + hex: 'd04d39c4e4b42e9edddbb741bb6636683b6822ae2c68df704b65ce4a', + }, + }, +]; diff --git a/tests/utils/trezor/transformations.test.ts b/tests/utils/trezor/transformations.test.ts index 88d7076..de63dfb 100644 --- a/tests/utils/trezor/transformations.test.ts +++ b/tests/utils/trezor/transformations.test.ts @@ -13,4 +13,10 @@ describe('trezor transformation utils', () => { ).toMatchObject(f.result); }); }); + + fixtures.drepIdToHex.forEach(f => { + test(f.description, () => { + expect(utils.drepIdToHex(f.drepId)).toStrictEqual(f.result); + }); + }); });