diff --git a/modules/sdk-coin-ada/src/lib/index.ts b/modules/sdk-coin-ada/src/lib/index.ts index e1282dc527..8c49f5ff7d 100644 --- a/modules/sdk-coin-ada/src/lib/index.ts +++ b/modules/sdk-coin-ada/src/lib/index.ts @@ -10,4 +10,5 @@ export { StakingClaimRewardsBuilder } from './stakingClaimRewardsBuilder'; export { StakingDeactivateBuilder } from './stakingDeactivateBuilder'; export { StakingWithdrawBuilder } from './stakingWithdrawBuilder'; export { StakingPledgeBuilder } from './stakingPledgeBuilder'; +export { VoteDelegationBuilder } from './voteDelegationBuilder'; export { Utils }; diff --git a/modules/sdk-coin-ada/src/lib/stakingActivateBuilder.ts b/modules/sdk-coin-ada/src/lib/stakingActivateBuilder.ts index 7baceb80ee..3dd8f9f5e6 100644 --- a/modules/sdk-coin-ada/src/lib/stakingActivateBuilder.ts +++ b/modules/sdk-coin-ada/src/lib/stakingActivateBuilder.ts @@ -3,6 +3,7 @@ import { BaseCoin as CoinConfig } from '@bitgo/statics'; import { TransactionBuilder } from './transactionBuilder'; import { Transaction } from './transaction'; import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs'; +import adaUtils from './utils'; export class StakingActivateBuilder extends TransactionBuilder { constructor(_coinConfig: Readonly) { @@ -25,7 +26,7 @@ export class StakingActivateBuilder extends TransactionBuilder { * @param stakingPublicKey The user's public stake key * @param poolHash Pool ID Hash of the pool we are going to delegate to */ - stakingCredential(stakingPublicKey: string, poolHash: string): this { + stakingCredential(stakingPublicKey: string, poolHash: string, dRepId: string): this { const stakeCredential = CardanoWasm.Credential.from_keyhash( CardanoWasm.PublicKey.from_bytes(Buffer.from(stakingPublicKey, 'hex')).hash() ); @@ -40,6 +41,10 @@ export class StakingActivateBuilder extends TransactionBuilder { ) ); this._certs.push(stakeDelegationCert); + const voteDelegationCert = CardanoWasm.Certificate.new_vote_delegation( + CardanoWasm.VoteDelegation.new(stakeCredential, adaUtils.getDRepFromDRepId(dRepId)) + ); + this._certs.push(voteDelegationCert); return this; } diff --git a/modules/sdk-coin-ada/src/lib/transaction.ts b/modules/sdk-coin-ada/src/lib/transaction.ts index b834b41dc7..686a8195d4 100644 --- a/modules/sdk-coin-ada/src/lib/transaction.ts +++ b/modules/sdk-coin-ada/src/lib/transaction.ts @@ -31,17 +31,19 @@ export interface Witness { publicKey: string; signature: string; } -enum CertType { +export enum CertType { StakeKeyRegistration, StakeKeyDelegation, StakeKeyDeregistration, StakePoolRegistration, + VoteDelegation, } export interface Cert { type: CertType; stakeCredentialHash?: string; poolKeyHash?: string; + dRepId?: string; } export interface Withdrawal { @@ -183,6 +185,14 @@ export class Transaction extends BaseTransaction { poolKeyHash: Buffer.from(stakePoolRegistration.pool_params().operator().to_bytes()).toString('hex'), }); } + if (cert.as_vote_delegation() !== undefined) { + const voteDelegation = cert.as_vote_delegation() as CardanoWasm.VoteDelegation; + result.certs.push({ + type: CertType.VoteDelegation, + stakeCredentialHash: Buffer.from(voteDelegation.stake_credential().to_bytes()).toString('hex'), + dRepId: Buffer.from(voteDelegation.drep().to_bytes()).toString('hex'), + }); + } } } @@ -279,6 +289,8 @@ export class Transaction extends BaseTransaction { this._type = TransactionType.StakingActivate; } else if (certs.some((c) => c.as_stake_deregistration() !== undefined)) { this._type = TransactionType.StakingDeactivate; + } else if (certs.some((c) => c.as_vote_delegation() !== undefined)) { + this._type = TransactionType.VoteDelegation; } } if (this._transaction.body().withdrawals()) { @@ -394,6 +406,8 @@ export class Transaction extends BaseTransaction { ? 'StakingDeactivate' : this._type === TransactionType.StakingPledge ? 'StakingPledge' + : this._type === TransactionType.VoteDelegation + ? 'VoteDelegation' : 'undefined'; return { displayOrder, diff --git a/modules/sdk-coin-ada/src/lib/transactionBuilder.ts b/modules/sdk-coin-ada/src/lib/transactionBuilder.ts index 2766106e8e..40b5bf7c6c 100644 --- a/modules/sdk-coin-ada/src/lib/transactionBuilder.ts +++ b/modules/sdk-coin-ada/src/lib/transactionBuilder.ts @@ -165,7 +165,7 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { const adjustment = BigNum.from_str('2000000'); let change = utxoBalance.checked_sub(this._fee).checked_sub(totalAmountToSend); - if (this._type === TransactionType.StakingActivate) { + if (this._type === TransactionType.StakingActivate || this._type === TransactionType.VoteDelegation) { change = change.checked_sub(adjustment); } else if (this._type === TransactionType.StakingDeactivate) { change = change.checked_add(adjustment); @@ -326,7 +326,7 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { const adjustment = BigNum.from_str('2000000'); let change = utxoBalance.checked_sub(this._fee).checked_sub(totalAmountToSend); - if (this._type === TransactionType.StakingActivate) { + if (this._type === TransactionType.StakingActivate || this._type === TransactionType.VoteDelegation) { change = change.checked_sub(adjustment); } else if (this._type === TransactionType.StakingDeactivate) { change = change.checked_add(adjustment); diff --git a/modules/sdk-coin-ada/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-ada/src/lib/transactionBuilderFactory.ts index 38cb2d3a7a..52e78f4626 100644 --- a/modules/sdk-coin-ada/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-ada/src/lib/transactionBuilderFactory.ts @@ -9,6 +9,7 @@ import { StakingDeactivateBuilder } from './stakingDeactivateBuilder'; import { StakingWithdrawBuilder } from './stakingWithdrawBuilder'; import { StakingPledgeBuilder } from './stakingPledgeBuilder'; import { StakingClaimRewardsBuilder } from './stakingClaimRewardsBuilder'; +import { VoteDelegationBuilder } from './voteDelegationBuilder'; export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { constructor(_coinConfig: Readonly) { @@ -38,6 +39,8 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { return this.getWalletInitializationBuilder(tx); case TransactionType.StakingPledge: return this.getStakingPledgeBuilder(tx); + case TransactionType.VoteDelegation: + return this.getVoteDelegationBuilder(tx); default: throw new InvalidTransactionError('unsupported transaction'); } @@ -60,19 +63,23 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { return TransactionBuilderFactory.initializeBuilder(tx, new StakingActivateBuilder(this._coinConfig)); } - getStakingClaimRewardsBuilder(tx?: Transaction) { + getVoteDelegationBuilder(tx?: Transaction): VoteDelegationBuilder { + return TransactionBuilderFactory.initializeBuilder(tx, new VoteDelegationBuilder(this._coinConfig)); + } + + getStakingClaimRewardsBuilder(tx?: Transaction): StakingClaimRewardsBuilder { return TransactionBuilderFactory.initializeBuilder(tx, new StakingClaimRewardsBuilder(this._coinConfig)); } - getStakingDeactivateBuilder(tx?: Transaction) { + getStakingDeactivateBuilder(tx?: Transaction): StakingDeactivateBuilder { return TransactionBuilderFactory.initializeBuilder(tx, new StakingDeactivateBuilder(this._coinConfig)); } - getStakingWithdrawBuilder(tx?: Transaction) { + getStakingWithdrawBuilder(tx?: Transaction): StakingWithdrawBuilder { return TransactionBuilderFactory.initializeBuilder(tx, new StakingWithdrawBuilder(this._coinConfig)); } - getStakingPledgeBuilder(tx?: Transaction) { + getStakingPledgeBuilder(tx?: Transaction): StakingPledgeBuilder { return TransactionBuilderFactory.initializeBuilder(tx, new StakingPledgeBuilder(this._coinConfig)); } diff --git a/modules/sdk-coin-ada/src/lib/utils.ts b/modules/sdk-coin-ada/src/lib/utils.ts index 771377acf5..5553771e95 100644 --- a/modules/sdk-coin-ada/src/lib/utils.ts +++ b/modules/sdk-coin-ada/src/lib/utils.ts @@ -1,3 +1,4 @@ +import assert from 'assert'; import { AddressFormat, BaseUtils } from '@bitgo/sdk-core'; import { BaseAddress, @@ -7,11 +8,15 @@ import { Credential, RewardAddress, Transaction as CardanoTransaction, + DRep, } from '@emurgo/cardano-serialization-lib-nodejs'; import { KeyPair } from './keyPair'; import { bech32 } from 'bech32'; export const MIN_ADA_FOR_ONE_ASSET = '1500000'; +export const VOTE_ALWAYS_ABSTAIN = 'always-abstain'; +export const VOTE_ALWAYS_NO_CONFIDENCE = 'always-no-confidence'; +export const DEFAULT_VOTE_OPTIONS = [VOTE_ALWAYS_ABSTAIN, VOTE_ALWAYS_NO_CONFIDENCE]; export class Utils implements BaseUtils { createBaseAddressWithStakeAndPaymentKey( @@ -75,6 +80,27 @@ export class Utils implements BaseUtils { return rewardAddress.to_address().to_bech32(); } + isValidDRepId(dRepId: string): boolean { + try { + DRep.from_bech32(dRepId); + return true; + } catch (err) { + return DEFAULT_VOTE_OPTIONS.includes(dRepId); + } + } + + getDRepFromDRepId(dRepId: string): DRep { + assert(this.isValidDRepId(dRepId), 'Invalid dRepId ' + dRepId); + switch (dRepId) { + case VOTE_ALWAYS_ABSTAIN: + return DRep.new_always_abstain(); + case VOTE_ALWAYS_NO_CONFIDENCE: + return DRep.new_always_no_confidence(); + default: + return DRep.from_bech32(dRepId); + } + } + /** @inheritdoc */ // this will validate both stake and payment addresses isValidAddress(address: string): boolean { diff --git a/modules/sdk-coin-ada/src/lib/voteDelegationBuilder.ts b/modules/sdk-coin-ada/src/lib/voteDelegationBuilder.ts new file mode 100644 index 0000000000..6c222f962c --- /dev/null +++ b/modules/sdk-coin-ada/src/lib/voteDelegationBuilder.ts @@ -0,0 +1,56 @@ +import { BaseKey, TransactionType } from '@bitgo/sdk-core'; +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { TransactionBuilder } from './transactionBuilder'; +import { Transaction } from './transaction'; +import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs'; +import adaUtils from './utils'; + +export class VoteDelegationBuilder extends TransactionBuilder { + constructor(_coinConfig: Readonly) { + super(_coinConfig); + this._type = TransactionType.VoteDelegation; + } + + protected get transactionType(): TransactionType { + return TransactionType.VoteDelegation; + } + + /** @inheritdoc */ + initBuilder(tx: Transaction): void { + super.initBuilder(tx); + } + + /** + * Creates the proper certificates needed to delegate a user's vote to a given DRep + * + * @param stakingPublicKey The user's public stake key + * @param dRepId The DRep ID of the DRep we will delegate vote to + */ + addVoteDelegationCertificate(stakingPublicKey: string, dRepId: string): this { + const stakeCredential = CardanoWasm.Credential.from_keyhash( + CardanoWasm.PublicKey.from_bytes(Buffer.from(stakingPublicKey, 'hex')).hash() + ); + const voteDelegationCert = CardanoWasm.Certificate.new_vote_delegation( + CardanoWasm.VoteDelegation.new(stakeCredential, adaUtils.getDRepFromDRepId(dRepId)) + ); + this._certs.push(voteDelegationCert); + return this; + } + + /** @inheritdoc */ + protected async buildImplementation(): Promise { + const tx = await super.buildImplementation(); + tx.setTransactionType(TransactionType.StakingActivate); + return tx; + } + + /** @inheritdoc */ + protected fromImplementation(rawTransaction: string): Transaction { + return super.fromImplementation(rawTransaction); + } + + /** @inheritdoc */ + protected signImplementation(key: BaseKey): Transaction { + return super.signImplementation(key); + } +} diff --git a/modules/sdk-coin-ada/test/resources/index.ts b/modules/sdk-coin-ada/test/resources/index.ts index 10e0a81e95..2b223c45b7 100644 --- a/modules/sdk-coin-ada/test/resources/index.ts +++ b/modules/sdk-coin-ada/test/resources/index.ts @@ -141,11 +141,16 @@ export const rawTx = { address: 'addr_test1vr8rakm66rcfv4fcxqykg5lf0yv7lsyk9mvapx369jpvtcgfcuk7f', value: '248329150', }, + unsignedVoteDelegationTx: + '84a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba2101018182583901c7b28bcea90d440b5455a6a02a27ca59b8696f067fc1967f47f933e79558e969caa9e57adcfc40b9907eb794363b590faf42fff48c38eb881a001bf227021a00029259031a2faf0800048183098200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb8200581c8b75035882d4165bea8000c4d3f2c123ae33c1d92a751a78135a2402a0f5f6', + unsignedVoteDelegationTxBody: + 'a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba2101018182583901c7b28bcea90d440b5455a6a02a27ca59b8696f067fc1967f47f933e79558e969caa9e57adcfc40b9907eb794363b590faf42fff48c38eb881a001bf227021a00029259031a2faf0800048183098200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb8200581c8b75035882d4165bea8000c4d3f2c123ae33c1d92a751a78135a2402', + unsignedVoteDelegationTxHash: 'c33c503919f9c5aa5a4ca40c0c6e4bb0099a7452a093d9ee01aff93958415e93', unsignedStakingActiveTx: - '84a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba2101018182583901c7b28bcea90d440b5455a6a02a27ca59b8696f067fc1967f47f933e79558e969caa9e57adcfc40b9907eb794363b590faf42fff48c38eb881a001beca7021a000297d9031a2faf0800048282008200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb83028200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb581c7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b3a0f5f6', + '84a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba2101018182583901c7b28bcea90d440b5455a6a02a27ca59b8696f067fc1967f47f933e79558e969caa9e57adcfc40b9907eb794363b590faf42fff48c38eb881a001be677021a00029e09031a2faf0800048382008200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb83028200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb581c7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b383098200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb8102a0f5f6', unsignedStakingActiveTxBody: - 'a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba2101018182583901c7b28bcea90d440b5455a6a02a27ca59b8696f067fc1967f47f933e79558e969caa9e57adcfc40b9907eb794363b590faf42fff48c38eb881a001beca7021a000297d9031a2faf0800048282008200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb83028200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb581c7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b3', - unsignedStakingActiveTxHash: '4e2777152ebc92c73daebc0075cee8a52ca9fb8c00511ac2c035b58c52f27125', + 'a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba2101018182583901c7b28bcea90d440b5455a6a02a27ca59b8696f067fc1967f47f933e79558e969caa9e57adcfc40b9907eb794363b590faf42fff48c38eb881a001be677021a00029e09031a2faf0800048382008200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb83028200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb581c7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b383098200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb8102', + unsignedStakingActiveTxHash: 'd8ecaa7bfee2e28691673be378ea6583bc8efd5a6e29ca9cfb278279a06dd216', unsignedStakingDeactiveTx: '84a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba2101018182583901c7b28bcea90d440b5455a6a02a27ca59b8696f067fc1967f47f933e79558e969caa9e57adcfc40b9907eb794363b590faf42fff48c38eb881a005900a7021a00028cd9031a2faf0800048182018200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdba0f5f6', unsignedStakingDeactiveTxBody: diff --git a/modules/sdk-coin-ada/test/unit/StakingActivateBuilder.ts b/modules/sdk-coin-ada/test/unit/StakingActivateBuilder.ts index 942b7bf342..02376a0b17 100644 --- a/modules/sdk-coin-ada/test/unit/StakingActivateBuilder.ts +++ b/modules/sdk-coin-ada/test/unit/StakingActivateBuilder.ts @@ -3,7 +3,7 @@ import { TransactionType, AddressFormat } from '@bitgo/sdk-core'; import * as testData from '../resources'; import { KeyPair, TransactionBuilderFactory } from '../../src'; import { coins } from '@bitgo/statics'; -import { Transaction } from '../../src/lib/transaction'; +import { CertType, Transaction } from '../../src/lib/transaction'; import * as Utils from '../../src/lib/utils'; describe('ADA Staking Activate Transaction Builder', async () => { @@ -21,19 +21,24 @@ describe('ADA Staking Activate Transaction Builder', async () => { 'addr1q8rm9z7w4yx5gz652kn2q238efvms6t0qelur9nlglun8eu4tr5knj4fu4adelzqhxg8adu5xca4jra0gtllfrpcawyq9psz23', totalInput ); - txBuilder.stakingCredential(keyPairStake.getKeys().pub, '7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b3'); + txBuilder.stakingCredential( + keyPairStake.getKeys().pub, + '7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b3', + 'always-abstain' + ); txBuilder.ttl(800000000); const tx = (await txBuilder.build()) as Transaction; should.equal(tx.type, TransactionType.StakingActivate); const txData = tx.toJson(); const fee = tx.getFee; - txData.certs.length.should.equal(2); + txData.certs.length.should.equal(3); txData.certs[0].type.should.equal(0); txData.certs[1].type.should.equal(1); + txData.certs[2].type.should.equal(CertType.VoteDelegation); txData.outputs.length.should.equal(1); txData.outputs[0].amount.should.equal((Number(totalInput) - 2000000 - Number(fee)).toString()); - fee.should.equal('169945'); + fee.should.equal('171529'); tx.toBroadcastFormat().should.equal(testData.rawTx.unsignedStakingActiveTx); should.equal(tx.id, testData.rawTx.unsignedStakingActiveTxHash); }); @@ -50,7 +55,11 @@ describe('ADA Staking Activate Transaction Builder', async () => { 'addr1q8rm9z7w4yx5gz652kn2q238efvms6t0qelur9nlglun8eu4tr5knj4fu4adelzqhxg8adu5xca4jra0gtllfrpcawyq9psz23', totalInput ); - txBuilder.stakingCredential(keyPairStake.getKeys().pub, '7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b3'); + txBuilder.stakingCredential( + keyPairStake.getKeys().pub, + '7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b3', + 'always-abstain' + ); txBuilder.ttl(800000000); const tx = (await txBuilder.build()) as Transaction; should.equal(tx.type, TransactionType.StakingActivate); @@ -129,7 +138,11 @@ describe('ADA Staking Activate Transaction Builder', async () => { const txBuilder = factory.getStakingActivateBuilder(); const senderBalance = '22122071'; txBuilder.changeAddress(senderAddress, senderBalance); - txBuilder.stakingCredential(keyPairStake.getKeys().pub, '7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b3'); + txBuilder.stakingCredential( + keyPairStake.getKeys().pub, + '7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b3', + 'always-abstain' + ); txBuilder.input({ transaction_id: '0a4f80d83ba9ce1f83306a79252909241308d7eff317d04c9ea018966d687fe3', @@ -160,44 +173,4 @@ describe('ADA Staking Activate Transaction Builder', async () => { const serializedTransaction2 = signedTransaction2.toBroadcastFormat(); serializedTransaction2.should.equal(serializedTransaction); }); - - it('should submit a staking transaction using one signature', async () => { - const keyPairPayment = new KeyPair({ prv: testData.privateKeys.prvKey9 }); - const keyPairStake = keyPairPayment; - const senderAddress = Utils.default.createBaseAddressWithStakeAndPaymentKey( - keyPairStake, - keyPairPayment, - AddressFormat.testnet - ); - const txBuilder = factory.getStakingActivateBuilder(); - const senderBalance = '22122071'; - txBuilder.changeAddress(senderAddress, senderBalance); - txBuilder.stakingCredential(keyPairStake.getKeys().pub, '7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b3'); - - txBuilder.input({ - transaction_id: '0a4f80d83ba9ce1f83306a79252909241308d7eff317d04c9ea018966d687fe3', - transaction_index: 0, - }); - - txBuilder.ttl(900000000); - - const unsignedTx = await txBuilder.build(); - const signableHex = unsignedTx.signablePayload.toString('hex'); - const serializedTx = unsignedTx.toBroadcastFormat(); - txBuilder.sign({ key: keyPairPayment.getKeys().prv }); - const signedTransaction = await txBuilder.build(); - const serializedTransaction = signedTransaction.toBroadcastFormat(); - - const txBuilder2 = factory.from(serializedTx); - const tx = await txBuilder2.build(); - tx.type.should.equal(TransactionType.StakingActivate); - const signableHex2 = tx.signablePayload.toString('hex'); - signableHex.should.equal(signableHex2); - const signaturePayment = keyPairPayment.signMessage(signableHex2); - txBuilder2.addSignature({ pub: keyPairPayment.getKeys().pub }, Buffer.from(signaturePayment)); - const signedTransaction2 = await txBuilder2.build(); - signedTransaction.id.should.equal(tx.id); - const serializedTransaction2 = signedTransaction2.toBroadcastFormat(); - serializedTransaction2.should.equal(serializedTransaction); - }); }); diff --git a/modules/sdk-coin-ada/test/unit/utils.ts b/modules/sdk-coin-ada/test/unit/utils.ts index 9debacdbb1..04b56fa997 100644 --- a/modules/sdk-coin-ada/test/unit/utils.ts +++ b/modules/sdk-coin-ada/test/unit/utils.ts @@ -1,7 +1,7 @@ import should from 'should'; import { KeyPair, Utils } from '../../src'; import { AddressFormat, toHex } from '@bitgo/sdk-core'; -import { Ed25519Signature } from '@emurgo/cardano-serialization-lib-nodejs'; +import { Ed25519Signature, DRep } from '@emurgo/cardano-serialization-lib-nodejs'; import { address, blockHash, @@ -128,10 +128,36 @@ describe('utils', () => { should.equal(Utils.default.isValidTransactionId('dalij43ta0ga2dadda02'), false); }); + it('should validate DRepId correctly', () => { + should.equal(Utils.default.isValidDRepId('always-abstain'), true); + should.equal(Utils.default.isValidDRepId('not-a-correct-choice'), false); + should.equal(Utils.default.isValidDRepId('always-no-confidence'), true); + should.equal(Utils.default.isValidDRepId('drep13d6sxkyz6st9h65qqrzd8ukpywhr8swe9f6357qntgjqye0gttd'), true); + should.equal(Utils.default.isValidDRepId('drep13d6sxkyz6st9h65qqrzd8ukpywhr8swe9f6357qntgjqye0incorrect'), false); + }); + + it('should get DRep entity from DRepId correctly', () => { + should.equal(Utils.default.getDRepFromDRepId('always-abstain').to_json(), DRep.new_always_abstain().to_json()); + should.equal( + Utils.default.getDRepFromDRepId('always-no-confidence').to_json(), + DRep.new_always_no_confidence().to_json() + ); + should.equal( + Utils.default.getDRepFromDRepId('drep13d6sxkyz6st9h65qqrzd8ukpywhr8swe9f6357qntgjqye0gttd').to_json(), + DRep.from_bech32('drep13d6sxkyz6st9h65qqrzd8ukpywhr8swe9f6357qntgjqye0gttd').to_json() + ); + should.throws( + () => Utils.default.getDRepFromDRepId('not-a-correct-choice').to_json(), + 'Invalid dRepId not-a-correct-choice' + ); + }); + it('should get transaction body correctly', () => { const { unsignedTx, unsignedTxBody, + unsignedVoteDelegationTx: unsignedStakingVoteDelegationOnlyTx, + unsignedVoteDelegationTxBody: unsignedStakingVoteDelegationOnlyTxBody, unsignedStakingActiveTx, unsignedStakingActiveTxBody, unsignedStakingDeactiveTx, @@ -145,6 +171,10 @@ describe('utils', () => { partiallySignedPledgeTx, } = rawTx; should.equal(Utils.default.getTransactionBody(unsignedTx), unsignedTxBody); + should.equal( + Utils.default.getTransactionBody(unsignedStakingVoteDelegationOnlyTx), + unsignedStakingVoteDelegationOnlyTxBody + ); should.equal(Utils.default.getTransactionBody(unsignedStakingActiveTx), unsignedStakingActiveTxBody); should.equal(Utils.default.getTransactionBody(unsignedStakingDeactiveTx), unsignedStakingDeactiveTxBody); should.equal(Utils.default.getTransactionBody(unsignedStakingWithdrawTx), unsignedStakingWithdrawTxBody); diff --git a/modules/sdk-coin-ada/test/unit/voteDelegationBuilder.ts b/modules/sdk-coin-ada/test/unit/voteDelegationBuilder.ts new file mode 100644 index 0000000000..8db123bd3d --- /dev/null +++ b/modules/sdk-coin-ada/test/unit/voteDelegationBuilder.ts @@ -0,0 +1,41 @@ +import should from 'should'; +import { TransactionType } from '@bitgo/sdk-core'; +import * as testData from '../resources'; +import { KeyPair, TransactionBuilderFactory } from '../../src'; +import { coins } from '@bitgo/statics'; +import { CertType, Transaction } from '../../src/lib/transaction'; + +describe('ADA Vote Delegation Transaction Builder', async () => { + const factory = new TransactionBuilderFactory(coins.get('tada')); + it('start and build an unsigned staking vote delegation tx', async () => { + const keyPairStake = new KeyPair({ prv: testData.privateKeys.prvKey2 }); + const txBuilder = factory.getVoteDelegationBuilder(); + txBuilder.input({ + transaction_id: '3677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba21', + transaction_index: 1, + }); + const totalInput = '4000000'; + txBuilder.changeAddress( + 'addr1q8rm9z7w4yx5gz652kn2q238efvms6t0qelur9nlglun8eu4tr5knj4fu4adelzqhxg8adu5xca4jra0gtllfrpcawyq9psz23', + totalInput + ); + txBuilder.addVoteDelegationCertificate( + keyPairStake.getKeys().pub, + 'drep13d6sxkyz6st9h65qqrzd8ukpywhr8swe9f6357qntgjqye0gttd' + ); + txBuilder.ttl(800000000); + const tx = (await txBuilder.build()) as Transaction; + console.log('------------------'); + console.log(tx.explainTransaction()); + should.equal(tx.type, TransactionType.StakingActivate); + const txData = tx.toJson(); + const fee = tx.getFee; + txData.certs.length.should.equal(1); + txData.certs[0].type.should.equal(CertType.VoteDelegation); + + txData.outputs.length.should.equal(1); + txData.outputs[0].amount.should.equal((Number(totalInput) - 2000000 - Number(fee)).toString()); + tx.toBroadcastFormat().should.equal(testData.rawTx.unsignedVoteDelegationTx); + should.equal(tx.id, testData.rawTx.unsignedVoteDelegationTxHash); + }); +}); diff --git a/modules/sdk-core/src/account-lib/baseCoin/enum.ts b/modules/sdk-core/src/account-lib/baseCoin/enum.ts index 94a5d0fb68..6b19d17f26 100644 --- a/modules/sdk-core/src/account-lib/baseCoin/enum.ts +++ b/modules/sdk-core/src/account-lib/baseCoin/enum.ts @@ -56,6 +56,8 @@ export enum TransactionType { StakingAdd, // Staking pledge (e.g. ADA) StakingPledge, + // Delegating vote (e.g. ADA) + VoteDelegation, // Staking Authorize (e.g. SOL) StakingAuthorize, // Staking Authorize from raw message (e.g. SOL)