Skip to content

Commit 7da169f

Browse files
authored
feat(sdk-coin-cosmos): add cosmos shared coin functionalities
2 parents fc1dc8a + a170bad commit 7da169f

File tree

9 files changed

+317
-8
lines changed

9 files changed

+317
-8
lines changed

modules/bitgo/test/browser/browser.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ describe('Coins', () => {
4747
Nep141Token: 1,
4848
WorldToken: 1,
4949
CosmosToken: 1,
50+
CosmosSharedCoin: 1,
5051
};
5152
Object.keys(BitGoJS.Coin)
5253
.filter((coinName) => !excludedKeys[coinName])

modules/sdk-coin-cosmos/package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@
4141
},
4242
"dependencies": {
4343
"@bitgo/abstract-cosmos": "^11.9.8",
44-
"@bitgo/sdk-core": "^35.8.0"
45-
}
44+
"@bitgo/sdk-core": "^35.8.0",
45+
"@bitgo/statics": "^55.2.0",
46+
"@cosmjs/amino": "^0.29.5",
47+
"@cosmjs/stargate": "^0.29.5",
48+
"bignumber.js": "^9.1.1"
49+
},
50+
"devDependencies": {}
4651
}
Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,90 @@
1-
import { BaseCoin, BitGoBase } from '@bitgo/sdk-core';
2-
import { CosmosCoin } from '@bitgo/abstract-cosmos';
1+
import assert from 'assert';
2+
import { BaseCoin, BitGoBase, common } from '@bitgo/sdk-core';
3+
import { CosmosCoin, GasAmountDetails, CosmosKeyPair } from '@bitgo/abstract-cosmos';
4+
import { BaseCoin as StaticsBaseCoin, CosmosNetwork } from '@bitgo/statics';
5+
import { KeyPair, Utils, TransactionBuilderFactory } from './lib';
36

47
/**
58
* Shared Cosmos coin implementation that uses configuration from statics
69
* instead of requiring individual coin modules
710
*/
811
export class CosmosSharedCoin extends CosmosCoin {
9-
protected constructor(bitgo: BitGoBase) {
10-
super(bitgo);
12+
private readonly _network: CosmosNetwork;
13+
14+
protected constructor(bitgo: BitGoBase, staticsCoin?: Readonly<StaticsBaseCoin>) {
15+
super(bitgo, staticsCoin);
16+
17+
if (!staticsCoin) {
18+
throw new Error('missing required constructor parameter staticsCoin');
19+
}
20+
21+
// Get the network configuration from statics
22+
const network = staticsCoin.network as CosmosNetwork;
23+
if (!network || !network.addressPrefix || !network.validatorPrefix) {
24+
throw new Error('Invalid network configuration: missing required Cosmos network parameters');
25+
}
26+
27+
this._network = network;
28+
}
29+
30+
static createInstance(bitgo: BitGoBase, staticsCoin?: Readonly<StaticsBaseCoin>): BaseCoin {
31+
return new CosmosSharedCoin(bitgo, staticsCoin);
32+
}
33+
34+
/** @inheritDoc **/
35+
getBaseFactor(): number {
36+
return Math.pow(10, this._staticsCoin.decimalPlaces);
37+
}
38+
39+
/** @inheritDoc **/
40+
getBuilder(): TransactionBuilderFactory {
41+
return new TransactionBuilderFactory(this._staticsCoin);
42+
}
43+
44+
/** @inheritDoc **/
45+
isValidAddress(address: string): boolean {
46+
const utils = new Utils(this._network);
47+
return utils.isValidAddress(address) || utils.isValidValidatorAddress(address);
48+
}
49+
50+
/** @inheritDoc **/
51+
getDenomination(): string {
52+
return this._network.denom;
53+
}
54+
55+
/** @inheritDoc **/
56+
getGasAmountDetails(): GasAmountDetails {
57+
return {
58+
gasAmount: this._network.gasAmount,
59+
gasLimit: this._network.gasLimit,
60+
};
61+
}
62+
63+
/**
64+
* Get the network configuration for this coin
65+
* @returns {CosmosNetwork} The network configuration
66+
*/
67+
getNetwork(): CosmosNetwork {
68+
return this._network;
69+
}
70+
71+
/** @inheritDoc **/
72+
getKeyPair(publicKey: string): CosmosKeyPair {
73+
return new KeyPair({ pub: publicKey }, this._staticsCoin);
74+
}
75+
76+
/** @inheritDoc **/
77+
getAddressFromPublicKey(pubKey: string): string {
78+
const keyPair = new KeyPair({ pub: pubKey }, this._staticsCoin);
79+
return keyPair.getAddress();
1180
}
1281

13-
static createInstance(bitgo: BitGoBase): BaseCoin {
14-
return new CosmosCoin(bitgo);
82+
/** @inheritDoc **/
83+
protected getPublicNodeUrl(): string {
84+
const family = this.getFamily();
85+
const env = this.bitgo.getEnv();
86+
const cosmosConfig = common.Environments[env]?.cosmos;
87+
assert(cosmosConfig?.[family]?.nodeUrl, `env config is missing for ${family} in ${env}`);
88+
return cosmosConfig[family].nodeUrl;
1589
}
1690
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
export { CosmosSharedCoin } from './cosmosSharedCoin';
2+
export { register } from './register';
3+
export { KeyPair } from './lib/keyPair';
4+
export { Utils } from './lib/utils';
5+
export { TransactionBuilderFactory } from './lib/transactionBuilderFactory';
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export {
2+
CosmosTransaction as Transaction,
3+
CosmosTransactionBuilder as TransactionBuilder,
4+
} from '@bitgo/abstract-cosmos';
5+
export { KeyPair } from './keyPair';
6+
export { TransactionBuilderFactory } from './transactionBuilderFactory';
7+
export { Utils } from './utils';
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { KeyPairOptions } from '@bitgo/sdk-core';
2+
import { pubkeyToAddress } from '@cosmjs/amino';
3+
import { CosmosKeyPair } from '@bitgo/abstract-cosmos';
4+
import { BaseCoin, CosmosNetwork } from '@bitgo/statics';
5+
6+
/**
7+
* Cosmos keys and address management for the shared Cosmos SDK.
8+
*/
9+
export class KeyPair extends CosmosKeyPair {
10+
private readonly _coin?: Readonly<BaseCoin>;
11+
12+
/**
13+
* Public constructor. By default, creates a key pair with a random master seed.
14+
*
15+
* @param source Either a master seed, a private key (extended or raw), or a public key
16+
* @param coin The coin configuration for this key pair
17+
*/
18+
constructor(source?: KeyPairOptions, coin?: Readonly<BaseCoin>) {
19+
super(source);
20+
this._coin = coin;
21+
}
22+
23+
/** @inheritdoc */
24+
getAddress(): string {
25+
if (!this._coin) {
26+
throw new Error('Coin configuration is required to derive address');
27+
}
28+
29+
const network = this._coin.network as CosmosNetwork;
30+
if (!network || !network.addressPrefix) {
31+
throw new Error('Invalid network configuration: missing addressPrefix');
32+
}
33+
34+
const addressPrefix = network.addressPrefix;
35+
const base64String = Buffer.from(this.getKeys().pub.slice(0, 66), 'hex').toString('base64');
36+
37+
return pubkeyToAddress(
38+
{
39+
type: 'tendermint/PubKeySecp256k1',
40+
value: base64String,
41+
},
42+
addressPrefix
43+
);
44+
}
45+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import {
2+
CosmosTransaction,
3+
CosmosTransactionBuilder,
4+
CosmosTransferBuilder,
5+
StakingActivateBuilder,
6+
StakingDeactivateBuilder,
7+
StakingRedelegateBuilder,
8+
StakingWithdrawRewardsBuilder,
9+
ContractCallBuilder,
10+
} from '@bitgo/abstract-cosmos';
11+
import { BaseCoin as CoinConfig, CosmosNetwork } from '@bitgo/statics';
12+
import { BaseTransactionBuilderFactory, InvalidTransactionError, TransactionType } from '@bitgo/sdk-core';
13+
import { Utils } from './utils';
14+
15+
export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
16+
private readonly _utils: Utils;
17+
18+
constructor(_coinConfig: Readonly<CoinConfig>) {
19+
super(_coinConfig);
20+
this._utils = new Utils(_coinConfig.network as CosmosNetwork);
21+
}
22+
23+
/** @inheritdoc */
24+
from(raw: string): CosmosTransactionBuilder {
25+
const tx = new CosmosTransaction(this._coinConfig, this._utils);
26+
tx.enrichTransactionDetailsFromRawTransaction(raw);
27+
try {
28+
switch (tx.type) {
29+
case TransactionType.Send:
30+
return this.getTransferBuilder(tx);
31+
case TransactionType.StakingActivate:
32+
return this.getStakingActivateBuilder(tx);
33+
case TransactionType.StakingDeactivate:
34+
return this.getStakingDeactivateBuilder(tx);
35+
case TransactionType.StakingWithdraw:
36+
return this.getStakingWithdrawRewardsBuilder(tx);
37+
case TransactionType.ContractCall:
38+
return this.getContractCallBuilder(tx);
39+
case TransactionType.StakingRedelegate:
40+
return this.getStakingRedelegateBuilder(tx);
41+
default:
42+
throw new InvalidTransactionError('Invalid transaction');
43+
}
44+
} catch (e) {
45+
throw new InvalidTransactionError('Invalid transaction: ' + e.message);
46+
}
47+
}
48+
49+
/** @inheritdoc */
50+
getTransferBuilder(tx?: CosmosTransaction): CosmosTransferBuilder {
51+
return this.initializeBuilder(tx, new CosmosTransferBuilder(this._coinConfig, this._utils));
52+
}
53+
54+
/** @inheritdoc */
55+
getStakingActivateBuilder(tx?: CosmosTransaction): StakingActivateBuilder {
56+
return this.initializeBuilder(tx, new StakingActivateBuilder(this._coinConfig, this._utils));
57+
}
58+
59+
/** @inheritdoc */
60+
getStakingDeactivateBuilder(tx?: CosmosTransaction): StakingDeactivateBuilder {
61+
return this.initializeBuilder(tx, new StakingDeactivateBuilder(this._coinConfig, this._utils));
62+
}
63+
64+
/** @inheritdoc */
65+
getStakingWithdrawRewardsBuilder(tx?: CosmosTransaction): StakingWithdrawRewardsBuilder {
66+
return this.initializeBuilder(tx, new StakingWithdrawRewardsBuilder(this._coinConfig, this._utils));
67+
}
68+
69+
/** @inheritdoc */
70+
getStakingRedelegateBuilder(tx?: CosmosTransaction): StakingRedelegateBuilder {
71+
return this.initializeBuilder(tx, new StakingRedelegateBuilder(this._coinConfig, this._utils));
72+
}
73+
74+
/** @inheritdoc */
75+
getContractCallBuilder(tx?: CosmosTransaction): ContractCallBuilder {
76+
return this.initializeBuilder(tx, new ContractCallBuilder(this._coinConfig, this._utils));
77+
}
78+
79+
/** @inheritdoc */
80+
getWalletInitializationBuilder(): void {
81+
throw new Error('Method not implemented.');
82+
}
83+
84+
/**
85+
* Initialize the builder with the given transaction
86+
*
87+
* @param {CosmosTransaction | undefined} tx - the transaction used to initialize the builder
88+
* @param {CosmosTransactionBuilder} builder - the builder to be initialized
89+
* @returns {CosmosTransactionBuilder} the builder initialized
90+
*/
91+
private initializeBuilder<T extends CosmosTransactionBuilder>(tx: CosmosTransaction | undefined, builder: T): T {
92+
if (tx) {
93+
builder.initBuilder(tx);
94+
}
95+
return builder;
96+
}
97+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { CosmosUtils } from '@bitgo/abstract-cosmos';
2+
import { CosmosNetwork } from '@bitgo/statics';
3+
import { Coin } from '@cosmjs/stargate';
4+
import { InvalidTransactionError } from '@bitgo/sdk-core';
5+
import BigNumber from 'bignumber.js';
6+
7+
/**
8+
* Cosmos utilities implementation using the shared Cosmos SDK
9+
*/
10+
export class Utils extends CosmosUtils {
11+
private readonly _network: CosmosNetwork;
12+
13+
constructor(network: CosmosNetwork) {
14+
super();
15+
this._network = network;
16+
}
17+
18+
/** @inheritdoc */
19+
isValidAddress(address: string): boolean {
20+
return this.isValidCosmosLikeAddressWithMemoId(
21+
address,
22+
new RegExp(`^${this._network.addressPrefix}1['qpzry9x8gf2tvdw0s3jn54khce6mua7l]{38}$`)
23+
);
24+
}
25+
26+
/** @inheritdoc */
27+
isValidValidatorAddress(address: string): boolean {
28+
return this.isValidBech32AddressMatchingRegex(
29+
address,
30+
new RegExp(`^${this._network.validatorPrefix}1['qpzry9x8gf2tvdw0s3jn54khce6mua7l]{38}$`)
31+
);
32+
}
33+
34+
/** @inheritdoc */
35+
isValidContractAddress(address: string): boolean {
36+
return this.isValidBech32AddressMatchingRegex(
37+
address,
38+
new RegExp(`^${this._network.addressPrefix}1['qpzry9x8gf2tvdw0s3jn54khce6mua7l]+$`)
39+
);
40+
}
41+
42+
/** @inheritdoc */
43+
validateAmount(amount: Coin): void {
44+
if (!amount?.denom) {
45+
throw new InvalidTransactionError('Invalid amount: missing denom');
46+
}
47+
if (amount.denom !== this._network.denom) {
48+
throw new InvalidTransactionError(
49+
`Invalid amount: denom '${amount.denom}' does not match network denom '${this._network.denom}'`
50+
);
51+
}
52+
if (!amount?.amount) {
53+
throw new InvalidTransactionError('Invalid amount: missing amount');
54+
}
55+
56+
const amountBN = new BigNumber(amount.amount);
57+
if (!amountBN.isFinite() || !amountBN.isInteger() || amountBN.isLessThanOrEqualTo(0)) {
58+
throw new InvalidTransactionError(`Invalid amount: '${amount.amount}' is not a valid positive integer`);
59+
}
60+
}
61+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { BitGoBase } from '@bitgo/sdk-core';
2+
import { CoinFeature, coins } from '@bitgo/statics';
3+
import { CosmosSharedCoin } from './cosmosSharedCoin';
4+
5+
/**
6+
* Register all coins that use the shared Cosmos SDK implementation
7+
* @param sdk BitGo instance
8+
*/
9+
export function register(sdk: BitGoBase): void {
10+
coins
11+
.filter((coin) => coin.features.includes(CoinFeature.SHARED_COSMOS_SDK))
12+
.forEach((coin) => {
13+
sdk.register(coin.name, CosmosSharedCoin.createInstance);
14+
});
15+
}

0 commit comments

Comments
 (0)