Skip to content

Commit daf91ba

Browse files
committed
feat(sdk-coin-baby): add WithdrawReward transaction
Ticket: SC-1640
1 parent 1286720 commit daf91ba

File tree

7 files changed

+212
-72
lines changed

7 files changed

+212
-72
lines changed

modules/sdk-coin-baby/src/lib/BabylonTransaction.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { CosmosTransaction, CosmosUtils, TransactionExplanation, TxData } from '@bitgo/abstract-cosmos';
22
import { Entry, InvalidTransactionError, TransactionRecipient, TransactionType } from '@bitgo/sdk-core';
3-
import { BabylonSpecificMessages } from './iface';
3+
import { BabylonSpecificMessages, WithdrawRewardMessage } from './iface';
44
import { BaseCoin as CoinConfig } from '@bitgo/statics';
5+
import { UNAVAILABLE_TEXT } from './constants';
56

67
export class BabylonTransaction extends CosmosTransaction<BabylonSpecificMessages> {
78
constructor(_coinConfig: Readonly<CoinConfig>, _utils: CosmosUtils<BabylonSpecificMessages>) {
@@ -18,7 +19,22 @@ export class BabylonTransaction extends CosmosTransaction<BabylonSpecificMessage
1819
case TransactionType.CustomTx:
1920
explanationResult.type = TransactionType.CustomTx;
2021
outputAmount = BigInt(0);
21-
outputs = [];
22+
outputs = json.sendMessages.flatMap((message) => {
23+
const value = message.value as BabylonSpecificMessages;
24+
switch (value._kind) {
25+
case 'CreateBtcDelegation':
26+
return [];
27+
case 'WithdrawReward':
28+
return [
29+
{
30+
address: (value as WithdrawRewardMessage).address,
31+
amount: UNAVAILABLE_TEXT,
32+
},
33+
];
34+
default:
35+
throw new InvalidTransactionError(`Unsupported BabylonSpecificMessages message`);
36+
}
37+
});
2238
break;
2339
default:
2440
return super.explainTransactionInternal(json, explanationResult);
@@ -44,6 +60,27 @@ export class BabylonTransaction extends CosmosTransaction<BabylonSpecificMessage
4460
const inputs: Entry[] = [];
4561
switch (this.type) {
4662
case TransactionType.CustomTx:
63+
this.cosmosLikeTransaction.sendMessages.forEach((message) => {
64+
const value = message.value as BabylonSpecificMessages;
65+
switch (value._kind) {
66+
case 'CreateBtcDelegation':
67+
break;
68+
case 'WithdrawReward':
69+
inputs.push({
70+
address: (value as WithdrawRewardMessage).address,
71+
value: UNAVAILABLE_TEXT,
72+
coin: this._coinConfig.name,
73+
});
74+
outputs.push({
75+
address: (value as WithdrawRewardMessage).address,
76+
value: UNAVAILABLE_TEXT,
77+
coin: this._coinConfig.name,
78+
});
79+
break;
80+
default:
81+
throw new InvalidTransactionError(`Unsupported BabylonSpecificMessages message`);
82+
}
83+
});
4784
break;
4885
default:
4986
return super.loadInputsAndOutputs();

modules/sdk-coin-baby/src/lib/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ export const contractAddressRegex = /^(bbn)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l]
55
export const ADDRESS_PREFIX = 'bbn';
66
export const GAS_AMOUNT = '7000';
77
export const GAS_LIMIT = 200000;
8+
export const UNAVAILABLE_TEXT = 'UNAVAILABLE';
89
export const wrappedDelegateMsgTypeUrl = '/babylon.epoching.v1.MsgWrappedDelegate';
910
export const wrappedUndelegateMsgTypeUrl = '/babylon.epoching.v1.MsgWrappedUndelegate';
1011
export const wrappedBeginRedelegateTypeUrl = '/babylon.epoching.v1.MsgWrappedBeginRedelegate';
1112
export const createBTCDelegationMsgTypeUrl = '/babylon.btcstaking.v1.MsgCreateBTCDelegation';
13+
export const withdrawRewardMsgTypeUrl = '/babylon.incentive.MsgWithdrawReward';
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import { btcstakingtx } from '@babylonlabs-io/babylon-proto-ts';
1+
import { btcstakingtx, incentivetx } from '@babylonlabs-io/babylon-proto-ts';
22

3-
export type BabylonSpecificMessageKind = 'CreateBtcDelegation';
3+
export type BabylonSpecificMessageKind = 'CreateBtcDelegation' | 'WithdrawReward';
44

55
type WithKind<T, Kind extends BabylonSpecificMessageKind> = T & {
66
_kind: Kind;
77
};
88

99
export type CreateBtcDelegationMessage = WithKind<btcstakingtx.MsgCreateBTCDelegation, 'CreateBtcDelegation'>;
10+
export type WithdrawRewardMessage = WithKind<incentivetx.MsgWithdrawReward, 'WithdrawReward'>;
1011

11-
export type BabylonSpecificMessages = CreateBtcDelegationMessage; // union other babylon specific message types here
12+
// union other babylon specific message types here
13+
export type BabylonSpecificMessages = CreateBtcDelegationMessage | WithdrawRewardMessage;

modules/sdk-coin-baby/src/lib/utils.ts

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
1-
import { epochingtx, btcstakingtx } from '@babylonlabs-io/babylon-proto-ts';
1+
import { epochingtx, btcstakingtx, incentivetx } from '@babylonlabs-io/babylon-proto-ts';
22
import { DecodedTxRaw } from '@cosmjs/proto-signing';
33
import { Coin } from '@cosmjs/stargate';
44
import BigNumber from 'bignumber.js';
55
import { Any } from 'cosmjs-types/google/protobuf/any';
6-
import { BabylonSpecificMessageKind, BabylonSpecificMessages } from './iface';
6+
import {
7+
BabylonSpecificMessageKind,
8+
BabylonSpecificMessages,
9+
CreateBtcDelegationMessage,
10+
WithdrawRewardMessage,
11+
} from './iface';
712
import { CosmosUtils, MessageData } from '@bitgo/abstract-cosmos';
813
import { InvalidTransactionError, TransactionType } from '@bitgo/sdk-core';
914
import * as constants from './constants';
1015

1116
export class Utils extends CosmosUtils<BabylonSpecificMessages> {
1217
public babylonMessageKindToTypeUrl: Record<BabylonSpecificMessageKind, string> = {
1318
CreateBtcDelegation: constants.createBTCDelegationMsgTypeUrl,
19+
WithdrawReward: constants.withdrawRewardMsgTypeUrl,
1420
};
1521
public babylonMessageTypeUrlToKind = Object.fromEntries(
1622
Object.entries(this.babylonMessageKindToTypeUrl).map(([key, value]) => [value, key])
@@ -29,6 +35,7 @@ export class Utils extends CosmosUtils<BabylonSpecificMessages> {
2935
this.registry.register(constants.wrappedUndelegateMsgTypeUrl, epochingtx.MsgWrappedUndelegate);
3036
this.registry.register(constants.wrappedBeginRedelegateTypeUrl, epochingtx.MsgWrappedBeginRedelegate);
3137
this.registry.register(constants.createBTCDelegationMsgTypeUrl, btcstakingtx.MsgCreateBTCDelegation);
38+
this.registry.register(constants.withdrawRewardMsgTypeUrl, incentivetx.MsgWithdrawReward);
3239
}
3340

3441
/** @inheritdoc */
@@ -86,6 +93,7 @@ export class Utils extends CosmosUtils<BabylonSpecificMessages> {
8693
case constants.wrappedBeginRedelegateTypeUrl:
8794
return TransactionType.StakingRedelegate;
8895
case constants.createBTCDelegationMsgTypeUrl:
96+
case constants.withdrawRewardMsgTypeUrl:
8997
return TransactionType.CustomTx;
9098
default:
9199
return super.getTransactionTypeFromTypeUrl(typeUrl);
@@ -110,13 +118,26 @@ export class Utils extends CosmosUtils<BabylonSpecificMessages> {
110118
}
111119

112120
/** @inheritdoc */
113-
validateCustomMessage(customMessage: BabylonSpecificMessages) {
114-
if (customMessage._kind !== 'CreateBtcDelegation') {
115-
throw new InvalidTransactionError(`Unsupported BabylonSpecificMessages message: ` + customMessage._kind);
121+
validateCustomMessage(customMessage: BabylonSpecificMessages): void {
122+
switch (customMessage._kind) {
123+
case 'CreateBtcDelegation':
124+
this.validateCreateBtcDelegationMessage(customMessage);
125+
break;
126+
case 'WithdrawReward':
127+
this.validateWithdrawRewardMessage(customMessage);
128+
break;
129+
default:
130+
throw new InvalidTransactionError(`Unsupported BabylonSpecificMessages message`);
131+
}
132+
}
133+
134+
validateCreateBtcDelegationMessage(createBtcDelegationMessage: CreateBtcDelegationMessage): void {
135+
if (createBtcDelegationMessage._kind !== 'CreateBtcDelegation') {
136+
throw new InvalidTransactionError(`Invalid CreateBtcDelegationMessage kind: ${createBtcDelegationMessage._kind}`);
116137
}
117138

118139
// TODO: check the other fields more thoroughly
119-
this.isObjPropertyNull(customMessage, [
140+
this.isObjPropertyNull(createBtcDelegationMessage, [
120141
'stakerAddr',
121142
// 'pop',
122143
'btcPk',
@@ -134,20 +155,38 @@ export class Utils extends CosmosUtils<BabylonSpecificMessages> {
134155
'delegatorUnbondingSlashingSig',
135156
]);
136157

137-
if (customMessage.pop) {
138-
this.isObjPropertyNull(customMessage.pop, ['btcSigType', 'btcSig']);
158+
if (createBtcDelegationMessage.pop) {
159+
this.isObjPropertyNull(createBtcDelegationMessage.pop, ['btcSigType', 'btcSig']);
139160
}
140161

141-
if (customMessage.stakingTxInclusionProof) {
142-
this.isObjPropertyNull(customMessage.stakingTxInclusionProof, ['key', 'proof']);
162+
if (createBtcDelegationMessage.stakingTxInclusionProof) {
163+
this.isObjPropertyNull(createBtcDelegationMessage.stakingTxInclusionProof, ['key', 'proof']);
143164

144-
if (customMessage.stakingTxInclusionProof.key) {
145-
this.isObjPropertyNull(customMessage.stakingTxInclusionProof.key, ['index', 'hash']);
165+
if (createBtcDelegationMessage.stakingTxInclusionProof.key) {
166+
this.isObjPropertyNull(createBtcDelegationMessage.stakingTxInclusionProof.key, ['index', 'hash']);
146167
}
147168
}
148169

149-
if (!this.isValidAddress(customMessage.stakerAddr)) {
150-
throw new InvalidTransactionError(`Invalid CreateBtcDelegationMessage stakerAddr: ` + customMessage.stakerAddr);
170+
if (!this.isValidAddress(createBtcDelegationMessage.stakerAddr)) {
171+
throw new InvalidTransactionError(
172+
`Invalid CreateBtcDelegationMessage stakerAddr: ${createBtcDelegationMessage.stakerAddr}`
173+
);
174+
}
175+
}
176+
177+
validateWithdrawRewardMessage(withdrawRewardMessage: WithdrawRewardMessage): void {
178+
if (withdrawRewardMessage._kind !== 'WithdrawReward') {
179+
throw new InvalidTransactionError(`Invalid WithdrawRewardMessage kind: ${withdrawRewardMessage._kind}`);
180+
}
181+
182+
this.isObjPropertyNull(withdrawRewardMessage, ['type', 'address']);
183+
184+
if (!['finality_provider', 'btc_staker'].includes(withdrawRewardMessage.type)) {
185+
throw new InvalidTransactionError(`Invalid WithdrawRewardMessage type: ${withdrawRewardMessage.type}`);
186+
}
187+
188+
if (!this.isValidAddress(withdrawRewardMessage.address)) {
189+
throw new InvalidTransactionError(`Invalid WithdrawRewardMessage address: ${withdrawRewardMessage.address}`);
151190
}
152191
}
153192

modules/sdk-coin-baby/test/resources/baby.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,55 @@ export const TEST_CUSTOM_MsgCreateBTCDelegation_TX = {
359359
],
360360
gasLimit: 200000,
361361
},
362+
inputs: [],
363+
outputs: [],
364+
};
365+
366+
export const TEST_CUSTOM_MsgWithdrawReward_TX = {
367+
hash: '',
368+
signature: 'KGFmGSxicrY4WjDoJPOaVyzd4TJNFtxVI6vLXCr1X7MaOylWJTh+2pkIJc5Cm/kUFkDqWsyHdUzexbt1ThyZ2A==',
369+
pubKey: 'AwT4xoruxA+DkKynr1LH7CM60RwR7Lp5InwQ5ISaN1Hw',
370+
privateKey: 'QAzeAkPWRGyRT8/TvJcRC7VSzQHV9QhH6YTmGZbnvmk=',
371+
signedTxBase64:
372+
'CmkKZwokL2JhYnlsb24uaW5jZW50aXZlLk1zZ1dpdGhkcmF3UmV3YXJkEj8KEWZpbmFsaXR5X3Byb3ZpZGVyEipiYm4xMjc0ZXA4cG5ybGVqNXZnbXR3cHB5c3luemNkNGZoeGMza3UwdDMSZQpQCkYKHy9jb3Ntb3MuY3J5cHRvLnNlY3AyNTZrMS5QdWJLZXkSIwohAwT4xoruxA+DkKynr1LH7CM60RwR7Lp5InwQ5ISaN1HwEgQKAggBGDsSEQoLCgR1YmJuEgM1MDAQwJoMGkAoYWYZLGJytjhaMOgk85pXLN3hMk0W3FUjq8tcKvVfsxo7KVYlOH7amQglzkKb+RQWQOpazId1TN7Fu3VOHJnY',
373+
from: 'bbn1274ep8pnrlej5vgmtwppysynzcd4fhxc3ku0t3',
374+
to: 'bbn1274ep8pnrlej5vgmtwppysynzcd4fhxc3ku0t3',
375+
chainId: 'bbn-test-5',
376+
accountNumber: 59235,
377+
sequence: 59,
378+
sendAmount: '0',
379+
feeAmount: '500',
380+
sendMessage: {
381+
typeUrl: '/babylon.incentive.MsgWithdrawReward',
382+
value: {
383+
_kind: 'WithdrawReward',
384+
type: 'finality_provider',
385+
address: 'bbn1274ep8pnrlej5vgmtwppysynzcd4fhxc3ku0t3',
386+
},
387+
},
388+
gasBudget: {
389+
amount: [
390+
{
391+
denom: 'ubbn',
392+
amount: '500',
393+
},
394+
],
395+
gasLimit: 200000,
396+
},
397+
inputs: [
398+
{
399+
address: 'bbn1274ep8pnrlej5vgmtwppysynzcd4fhxc3ku0t3',
400+
value: 'UNAVAILABLE',
401+
coin: 'tbaby',
402+
},
403+
],
404+
outputs: [
405+
{
406+
address: 'bbn1274ep8pnrlej5vgmtwppysynzcd4fhxc3ku0t3',
407+
value: 'UNAVAILABLE',
408+
coin: 'tbaby',
409+
},
410+
],
362411
};
363412

364413
export const address = {

modules/sdk-coin-baby/test/unit/transactionBuilder/CustomTransactionBuilder.ts

Lines changed: 59 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -11,75 +11,81 @@ describe('Baby Custom txn Builder', () => {
1111
let bitgo: TestBitGoAPI;
1212
let basecoin;
1313
let factory;
14-
let testTx;
14+
let testTxs;
1515
before(function () {
1616
bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' });
1717
bitgo.safeRegister('baby', Baby.createInstance);
1818
bitgo.safeRegister('tbaby', Tbaby.createInstance);
1919
bitgo.initializeTestVars();
2020
basecoin = bitgo.coin('tbaby');
2121
factory = basecoin.getBuilder();
22-
testTx = testData.TEST_CUSTOM_MsgCreateBTCDelegation_TX;
22+
testTxs = [testData.TEST_CUSTOM_MsgCreateBTCDelegation_TX, testData.TEST_CUSTOM_MsgWithdrawReward_TX];
2323
});
2424

2525
it('should build a Custom tx with signature', async function () {
26-
const txBuilder = factory.getCustomTransactionBuilder();
27-
txBuilder.sequence(testTx.sequence);
28-
txBuilder.gasBudget(testTx.gasBudget);
29-
txBuilder.messages([testTx.sendMessage.value]);
30-
txBuilder.publicKey(toHex(fromBase64(testTx.pubKey)));
31-
txBuilder.addSignature({ pub: toHex(fromBase64(testTx.pubKey)) }, Buffer.from(testTx.signature, 'base64'));
26+
for (const testTx of testTxs) {
27+
const txBuilder = factory.getCustomTransactionBuilder();
28+
txBuilder.sequence(testTx.sequence);
29+
txBuilder.gasBudget(testTx.gasBudget);
30+
txBuilder.messages([testTx.sendMessage.value]);
31+
txBuilder.publicKey(toHex(fromBase64(testTx.pubKey)));
32+
txBuilder.addSignature({ pub: toHex(fromBase64(testTx.pubKey)) }, Buffer.from(testTx.signature, 'base64'));
3233

33-
const tx = await txBuilder.build();
34-
const json = await (await txBuilder.build()).toJson();
35-
should.equal(tx.type, TransactionType.CustomTx);
36-
should.deepEqual(json.gasBudget, testTx.gasBudget);
37-
should.deepEqual(json.sendMessages, [testTx.sendMessage]);
38-
should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey)));
39-
should.deepEqual(json.sequence, testTx.sequence);
40-
const rawTx = tx.toBroadcastFormat();
41-
should.equal(rawTx, testTx.signedTxBase64);
42-
should.deepEqual(tx.inputs, []);
43-
should.deepEqual(tx.outputs, []);
34+
const tx = await txBuilder.build();
35+
const json = await (await txBuilder.build()).toJson();
36+
should.equal(tx.type, TransactionType.CustomTx);
37+
should.deepEqual(json.gasBudget, testTx.gasBudget);
38+
should.deepEqual(json.sendMessages, [testTx.sendMessage]);
39+
should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey)));
40+
should.deepEqual(json.sequence, testTx.sequence);
41+
const rawTx = tx.toBroadcastFormat();
42+
should.equal(rawTx, testTx.signedTxBase64);
43+
should.deepEqual(tx.inputs, testTx.inputs);
44+
should.deepEqual(tx.outputs, testTx.outputs);
45+
}
4446
});
4547

4648
it('should build a Custom tx without signature', async function () {
47-
const txBuilder = factory.getCustomTransactionBuilder();
48-
txBuilder.sequence(testTx.sequence);
49-
txBuilder.gasBudget(testTx.gasBudget);
50-
txBuilder.messages([testTx.sendMessage.value]);
51-
txBuilder.publicKey(toHex(fromBase64(testTx.pubKey)));
52-
const tx = await txBuilder.build();
53-
const json = await (await txBuilder.build()).toJson();
54-
should.equal(tx.type, TransactionType.CustomTx);
55-
should.deepEqual(json.gasBudget, testTx.gasBudget);
56-
should.deepEqual(json.sendMessages, [testTx.sendMessage]);
57-
should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey)));
58-
should.deepEqual(json.sequence, testTx.sequence);
59-
tx.toBroadcastFormat();
60-
should.deepEqual(tx.inputs, []);
61-
should.deepEqual(tx.outputs, []);
49+
for (const testTx of testTxs) {
50+
const txBuilder = factory.getCustomTransactionBuilder();
51+
txBuilder.sequence(testTx.sequence);
52+
txBuilder.gasBudget(testTx.gasBudget);
53+
txBuilder.messages([testTx.sendMessage.value]);
54+
txBuilder.publicKey(toHex(fromBase64(testTx.pubKey)));
55+
const tx = await txBuilder.build();
56+
const json = await (await txBuilder.build()).toJson();
57+
should.equal(tx.type, TransactionType.CustomTx);
58+
should.deepEqual(json.gasBudget, testTx.gasBudget);
59+
should.deepEqual(json.sendMessages, [testTx.sendMessage]);
60+
should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey)));
61+
should.deepEqual(json.sequence, testTx.sequence);
62+
tx.toBroadcastFormat();
63+
should.deepEqual(tx.inputs, testTx.inputs);
64+
should.deepEqual(tx.outputs, testTx.outputs);
65+
}
6266
});
6367

6468
it('should sign a Custom tx', async function () {
65-
const txBuilder = factory.getCustomTransactionBuilder();
66-
txBuilder.sequence(testTx.sequence);
67-
txBuilder.gasBudget(testTx.gasBudget);
68-
txBuilder.messages([testTx.sendMessage.value]);
69-
txBuilder.accountNumber(testTx.accountNumber);
70-
txBuilder.chainId(testTx.chainId);
71-
txBuilder.sign({ key: toHex(fromBase64(testTx.privateKey)) });
72-
const tx = await txBuilder.build();
73-
const json = await (await txBuilder.build()).toJson();
74-
should.equal(tx.type, TransactionType.CustomTx);
75-
should.deepEqual(json.gasBudget, testTx.gasBudget);
76-
should.deepEqual(json.sendMessages, [testTx.sendMessage]);
77-
should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey)));
78-
should.deepEqual(json.sequence, testTx.sequence);
79-
const rawTx = tx.toBroadcastFormat();
80-
should.equal(tx.signature[0], toHex(fromBase64(testTx.signature)));
81-
should.equal(rawTx, testTx.signedTxBase64);
82-
should.deepEqual(tx.inputs, []);
83-
should.deepEqual(tx.outputs, []);
69+
for (const testTx of testTxs) {
70+
const txBuilder = factory.getCustomTransactionBuilder();
71+
txBuilder.sequence(testTx.sequence);
72+
txBuilder.gasBudget(testTx.gasBudget);
73+
txBuilder.messages([testTx.sendMessage.value]);
74+
txBuilder.accountNumber(testTx.accountNumber);
75+
txBuilder.chainId(testTx.chainId);
76+
txBuilder.sign({ key: toHex(fromBase64(testTx.privateKey)) });
77+
const tx = await txBuilder.build();
78+
const json = await (await txBuilder.build()).toJson();
79+
should.equal(tx.type, TransactionType.CustomTx);
80+
should.deepEqual(json.gasBudget, testTx.gasBudget);
81+
should.deepEqual(json.sendMessages, [testTx.sendMessage]);
82+
should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey)));
83+
should.deepEqual(json.sequence, testTx.sequence);
84+
const rawTx = tx.toBroadcastFormat();
85+
should.equal(tx.signature[0], toHex(fromBase64(testTx.signature)));
86+
should.equal(rawTx, testTx.signedTxBase64);
87+
should.deepEqual(tx.inputs, testTx.inputs);
88+
should.deepEqual(tx.outputs, testTx.outputs);
89+
}
8490
});
8591
});

0 commit comments

Comments
 (0)