diff --git a/Dockerfile b/Dockerfile index d8a34ff896..66cf3e1045 100644 --- a/Dockerfile +++ b/Dockerfile @@ -142,6 +142,8 @@ COPY --from=builder /tmp/bitgo/modules/sdk-coin-eos /var/modules/sdk-coin-eos/ RUN cd /var/modules/sdk-coin-eos && yarn link COPY --from=builder /tmp/bitgo/modules/sdk-coin-ethw /var/modules/sdk-coin-ethw/ RUN cd /var/modules/sdk-coin-ethw && yarn link +COPY --from=builder /tmp/bitgo/modules/sdk-coin-injective /var/modules/sdk-coin-injective/ +RUN cd /var/modules/sdk-coin-injective && yarn link COPY --from=builder /tmp/bitgo/modules/sdk-coin-ltc /var/modules/sdk-coin-ltc/ RUN cd /var/modules/sdk-coin-ltc && yarn link COPY --from=builder /tmp/bitgo/modules/sdk-coin-xlm /var/modules/sdk-coin-xlm/ @@ -206,6 +208,7 @@ RUN cd /var/bitgo-express && \ yarn link @bitgo/sdk-coin-doge && \ yarn link @bitgo/sdk-coin-eos && \ yarn link @bitgo/sdk-coin-ethw && \ + yarn link @bitgo/sdk-coin-injective && \ yarn link @bitgo/sdk-coin-ltc && \ yarn link @bitgo/sdk-coin-xlm && \ yarn link @bitgo/sdk-coin-xrp && \ @@ -213,9 +216,9 @@ RUN cd /var/bitgo-express && \ #LINK_END #LABEL_START -LABEL created="Wed, 21 Jun 2023 04:11:17 GMT" +LABEL created="Thu, 22 Jun 2023 18:10:16 GMT" LABEL version=9.29.0 -LABEL git_hash=fced576cf32a20224ba25abd31efdfd4e8e04f94 +LABEL git_hash=e8bf1cceea1bff0ee05a38b6f8bbe04c4c4b5588 #LABEL_END USER node diff --git a/modules/account-lib/package.json b/modules/account-lib/package.json index 5a4148671e..af967c756e 100644 --- a/modules/account-lib/package.json +++ b/modules/account-lib/package.json @@ -41,6 +41,7 @@ "@bitgo/sdk-coin-eth2": "^1.2.27", "@bitgo/sdk-coin-hash": "^1.1.0", "@bitgo/sdk-coin-hbar": "^1.3.27", + "@bitgo/sdk-coin-injective": "^1.1.2", "@bitgo/sdk-coin-near": "^1.4.5", "@bitgo/sdk-coin-osmo": "^1.3.2", "@bitgo/sdk-coin-polygon": "^1.5.11", diff --git a/modules/account-lib/src/index.ts b/modules/account-lib/src/index.ts index c905983a2d..c1352800d3 100644 --- a/modules/account-lib/src/index.ts +++ b/modules/account-lib/src/index.ts @@ -83,6 +83,9 @@ export { Hash }; import * as Sei from '@bitgo/sdk-coin-sei'; export { Sei }; +import * as Injective from '@bitgo/sdk-coin-injective'; +export { Injective }; + import * as Sol from '@bitgo/sdk-coin-sol'; export { Sol }; @@ -150,6 +153,8 @@ const coinBuilderMap = { thash: Hash.TransactionBuilderFactory, sei: Sei.TransactionBuilderFactory, tsei: Sei.TransactionBuilderFactory, + injective: Injective.TransactionBuilderFactory, + tinjective: Injective.TransactionBuilderFactory, }; /** diff --git a/modules/account-lib/tsconfig.json b/modules/account-lib/tsconfig.json index 032073d383..3c2df2a97e 100644 --- a/modules/account-lib/tsconfig.json +++ b/modules/account-lib/tsconfig.json @@ -49,6 +49,9 @@ { "path": "../sdk-coin-hbar" }, + { + "path": "../sdk-coin-injective" + }, { "path": "../sdk-coin-near" }, diff --git a/modules/bitgo/package.json b/modules/bitgo/package.json index 3faac0ffc3..1c5efa9d53 100644 --- a/modules/bitgo/package.json +++ b/modules/bitgo/package.json @@ -72,6 +72,7 @@ "@bitgo/sdk-coin-ethw": "^1.3.27", "@bitgo/sdk-coin-hash": "^1.1.0", "@bitgo/sdk-coin-hbar": "^1.3.27", + "@bitgo/sdk-coin-injective": "^1.1.2", "@bitgo/sdk-coin-ltc": "^2.0.22", "@bitgo/sdk-coin-near": "^1.4.5", "@bitgo/sdk-coin-osmo": "^1.3.2", diff --git a/modules/bitgo/src/v2/coinFactory.ts b/modules/bitgo/src/v2/coinFactory.ts index be4e18d801..b7772f8d69 100644 --- a/modules/bitgo/src/v2/coinFactory.ts +++ b/modules/bitgo/src/v2/coinFactory.ts @@ -43,6 +43,7 @@ import { Gteth, Hash, Hbar, + Injective, Ltc, Ofc, OfcToken, @@ -81,6 +82,7 @@ import { Thash, Thbar, Tia, + Tinjective, Tltc, Tosmo, Tpolygon, @@ -133,6 +135,7 @@ function registerCoinConstructors(globalCoinFactory: CoinFactory): void { globalCoinFactory.register('hash', Hash.createInstance); globalCoinFactory.register('hbar', Hbar.createInstance); globalCoinFactory.register('ltc', Ltc.createInstance); + globalCoinFactory.register('injective', Injective.createInstance); globalCoinFactory.register('near', Near.createInstance); globalCoinFactory.register('ofc', Ofc.createInstance); globalCoinFactory.register('osmo', Osmo.createInstance); @@ -169,6 +172,7 @@ function registerCoinConstructors(globalCoinFactory: CoinFactory): void { globalCoinFactory.register('tfiatusd', TfiatUsd.createInstance); globalCoinFactory.register('thash', Thash.createInstance); globalCoinFactory.register('thbar', Thbar.createInstance); + globalCoinFactory.register('tinjective', Tinjective.createInstance); globalCoinFactory.register('tltc', Tltc.createInstance); globalCoinFactory.register('tnear', TNear.createInstance); globalCoinFactory.register('tosmo', Tosmo.createInstance); diff --git a/modules/bitgo/src/v2/coins/index.ts b/modules/bitgo/src/v2/coins/index.ts index 6783c4fe46..f659374810 100644 --- a/modules/bitgo/src/v2/coins/index.ts +++ b/modules/bitgo/src/v2/coins/index.ts @@ -1,82 +1,85 @@ import { AbstractUtxoCoin } from '@bitgo/abstract-utxo'; -export { AbstractUtxoCoin }; +import { Ada, Tada } from '@bitgo/sdk-coin-ada'; import { Algo, AlgoToken, Talgo } from '@bitgo/sdk-coin-algo'; +import { Atom, Tatom } from '@bitgo/sdk-coin-atom'; +import { AvaxC, AvaxCToken, TavaxC } from '@bitgo/sdk-coin-avaxc'; +import { AvaxP, TavaxP } from '@bitgo/sdk-coin-avaxp'; +import { Bch, Tbch } from '@bitgo/sdk-coin-bch'; +import { Bcha, Tbcha } from '@bitgo/sdk-coin-bcha'; +import { Bld, Tbld } from '@bitgo/sdk-coin-bld'; +import { Bsc, BscToken, Tbsc } from '@bitgo/sdk-coin-bsc'; +import { Bsv, Tbsv } from '@bitgo/sdk-coin-bsv'; +import { Btc, Tbtc } from '@bitgo/sdk-coin-btc'; +import { Btg } from '@bitgo/sdk-coin-btg'; +import { Celo, CeloToken, Tcelo } from '@bitgo/sdk-coin-celo'; +import { Cspr, Tcspr } from '@bitgo/sdk-coin-cspr'; +import { Dash, Tdash } from '@bitgo/sdk-coin-dash'; +import { Doge, Tdoge } from '@bitgo/sdk-coin-doge'; +import { Dot, Tdot } from '@bitgo/sdk-coin-dot'; +import { Eos, EosToken, Teos } from '@bitgo/sdk-coin-eos'; +import { Etc, Tetc } from '@bitgo/sdk-coin-etc'; +import { Erc20Token, Eth, Gteth, Teth } from '@bitgo/sdk-coin-eth'; +import { Eth2, Teth2 } from '@bitgo/sdk-coin-eth2'; +import { Ethw } from '@bitgo/sdk-coin-ethw'; +import { Hash, Thash } from '@bitgo/sdk-coin-hash'; +import { Hbar, Thbar } from '@bitgo/sdk-coin-hbar'; +import { Injective, Tinjective } from '@bitgo/sdk-coin-injective'; +import { Ltc, Tltc } from '@bitgo/sdk-coin-ltc'; +import { Osmo, Tosmo } from '@bitgo/sdk-coin-osmo'; +import { Polygon, PolygonToken, Tpolygon } from '@bitgo/sdk-coin-polygon'; +import { Rbtc, Trbtc } from '@bitgo/sdk-coin-rbtc'; +import { Sei, Tsei } from '@bitgo/sdk-coin-sei'; +import { Sol, Tsol } from '@bitgo/sdk-coin-sol'; +import { Stx, Tstx } from '@bitgo/sdk-coin-stx'; +import { Sui, Tsui } from '@bitgo/sdk-coin-sui'; +import { Tia, Ttia } from '@bitgo/sdk-coin-tia'; +import { Trx, Ttrx } from '@bitgo/sdk-coin-trx'; +import { StellarToken, Txlm, Xlm } from '@bitgo/sdk-coin-xlm'; +import { Txrp, Xrp } from '@bitgo/sdk-coin-xrp'; +import { Txtz, Xtz } from '@bitgo/sdk-coin-xtz'; +import { Tzec, Zec } from '@bitgo/sdk-coin-zec'; +export { AbstractUtxoCoin }; export { Algo, AlgoToken, Talgo }; -import { Ada, Tada } from '@bitgo/sdk-coin-ada'; export { Ada, Tada }; -import { Atom, Tatom } from '@bitgo/sdk-coin-atom'; export { Atom, Tatom }; -import { AvaxC, TavaxC, AvaxCToken } from '@bitgo/sdk-coin-avaxc'; -export { AvaxC, TavaxC, AvaxCToken }; -import { AvaxP, TavaxP } from '@bitgo/sdk-coin-avaxp'; +export { AvaxC, AvaxCToken, TavaxC }; export { AvaxP, TavaxP }; -import { Bch, Tbch } from '@bitgo/sdk-coin-bch'; export { Bch, Tbch }; -import { Bsc, Tbsc, BscToken } from '@bitgo/sdk-coin-bsc'; -export { Bsc, Tbsc, BscToken }; -import { Bsv, Tbsv } from '@bitgo/sdk-coin-bsv'; +export { Bsc, BscToken, Tbsc }; export { Bsv, Tbsv }; -import { Btc, Tbtc } from '@bitgo/sdk-coin-btc'; export { Btc, Tbtc }; -import { Btg } from '@bitgo/sdk-coin-btg'; export { Btg }; -import { Celo, CeloToken, Tcelo } from '@bitgo/sdk-coin-celo'; export { Celo, CeloToken, Tcelo }; -import { Cspr, Tcspr } from '@bitgo/sdk-coin-cspr'; export { Cspr, Tcspr }; -import { Dash, Tdash } from '@bitgo/sdk-coin-dash'; export { Dash, Tdash }; -import { Doge, Tdoge } from '@bitgo/sdk-coin-doge'; export { Doge, Tdoge }; -import { Dot, Tdot } from '@bitgo/sdk-coin-dot'; export { Dot, Tdot }; -import { Bcha, Tbcha } from '@bitgo/sdk-coin-bcha'; export { Bcha, Tbcha }; -import { EosToken, Eos, Teos } from '@bitgo/sdk-coin-eos'; -export { EosToken, Eos, Teos }; -import { Erc20Token, Eth, Gteth, Teth } from '@bitgo/sdk-coin-eth'; +export { Eos, EosToken, Teos }; export { Erc20Token, Eth, Gteth, Teth }; -import { Eth2, Teth2 } from '@bitgo/sdk-coin-eth2'; export { Eth2, Teth2 }; -import { Ethw } from '@bitgo/sdk-coin-ethw'; export { Ethw }; -import { Etc, Tetc } from '@bitgo/sdk-coin-etc'; export { Etc, Tetc }; -import { Hash, Thash } from '@bitgo/sdk-coin-hash'; export { Hash, Thash }; -import { Hbar, Thbar } from '@bitgo/sdk-coin-hbar'; export { Hbar, Thbar }; -import { Ltc, Tltc } from '@bitgo/sdk-coin-ltc'; export { Ltc, Tltc }; -import { Osmo, Tosmo } from '@bitgo/sdk-coin-osmo'; export { Osmo, Tosmo }; -import { Polygon, Tpolygon, PolygonToken } from '@bitgo/sdk-coin-polygon'; -export { Polygon, Tpolygon, PolygonToken }; -import { Rbtc, Trbtc } from '@bitgo/sdk-coin-rbtc'; +export { Polygon, PolygonToken, Tpolygon }; export { Rbtc, Trbtc }; -import { Sol, Tsol } from '@bitgo/sdk-coin-sol'; export { Sol, Tsol }; -import { Stx, Tstx } from '@bitgo/sdk-coin-stx'; export { Stx, Tstx }; -import { Sui, Tsui } from '@bitgo/sdk-coin-sui'; export { Sui, Tsui }; -import { Tia, Ttia } from '@bitgo/sdk-coin-tia'; export { Tia, Ttia }; -import { Bld, Tbld } from '@bitgo/sdk-coin-bld'; export { Bld, Tbld }; -import { Sei, Tsei } from '@bitgo/sdk-coin-sei'; export { Sei, Tsei }; -import { Trx, Ttrx } from '@bitgo/sdk-coin-trx'; +export { Injective, Tinjective }; export { Trx, Ttrx }; -import { StellarToken, Txlm, Xlm } from '@bitgo/sdk-coin-xlm'; export { StellarToken, Txlm, Xlm }; -import { Xrp, Txrp } from '@bitgo/sdk-coin-xrp'; -export { Xrp, Txrp }; -import { Xtz, Txtz } from '@bitgo/sdk-coin-xtz'; -export { Xtz, Txtz }; -import { Zec, Tzec } from '@bitgo/sdk-coin-zec'; -export { Zec, Tzec }; +export { Txrp, Xrp }; +export { Txtz, Xtz }; +export { Tzec, Zec }; import { coins } from '@bitgo/sdk-core'; const { Ofc, OfcToken, Susd, FiatUsd, FiatEur, FiatGBP, Tsusd, TfiatUsd, TfiatEur, TfiatGBP } = coins; -export { Ofc, OfcToken, Susd, FiatUsd, FiatEur, FiatGBP, Tsusd, TfiatUsd, TfiatEur, TfiatGBP }; +export { FiatEur, FiatGBP, FiatUsd, Ofc, OfcToken, Susd, TfiatEur, TfiatGBP, TfiatUsd, Tsusd }; + diff --git a/modules/bitgo/tsconfig.json b/modules/bitgo/tsconfig.json index 9479c63f7a..eba8381b18 100644 --- a/modules/bitgo/tsconfig.json +++ b/modules/bitgo/tsconfig.json @@ -107,6 +107,9 @@ { "path": "../sdk-coin-hbar" }, + { + "path": "../sdk-coin-injective" + }, { "path": "../sdk-coin-ltc" }, diff --git a/modules/sdk-coin-injective/.eslintignore b/modules/sdk-coin-injective/.eslintignore new file mode 100644 index 0000000000..190f83e0df --- /dev/null +++ b/modules/sdk-coin-injective/.eslintignore @@ -0,0 +1,5 @@ +node_modules +.idea +public +dist + diff --git a/modules/sdk-coin-injective/.gitignore b/modules/sdk-coin-injective/.gitignore new file mode 100644 index 0000000000..67ccce4c64 --- /dev/null +++ b/modules/sdk-coin-injective/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.idea/ +dist/ diff --git a/modules/sdk-coin-injective/.mocharc.yml b/modules/sdk-coin-injective/.mocharc.yml new file mode 100644 index 0000000000..95814796d1 --- /dev/null +++ b/modules/sdk-coin-injective/.mocharc.yml @@ -0,0 +1,8 @@ +require: 'ts-node/register' +timeout: '60000' +reporter: 'min' +reporter-option: + - 'cdn=true' + - 'json=false' +exit: true +spec: ['test/unit/**/*.ts'] diff --git a/modules/sdk-coin-injective/.npmignore b/modules/sdk-coin-injective/.npmignore new file mode 100644 index 0000000000..d5fb3a098c --- /dev/null +++ b/modules/sdk-coin-injective/.npmignore @@ -0,0 +1,14 @@ +!dist/ +dist/test/ +dist/tsconfig.tsbuildinfo +.idea/ +.prettierrc.yml +tsconfig.json +src/ +test/ +scripts/ +.nyc_output +CODEOWNERS +node_modules/ +.prettierignore +.mocharc.js diff --git a/modules/sdk-coin-injective/.prettierignore b/modules/sdk-coin-injective/.prettierignore new file mode 100644 index 0000000000..3a11d6af29 --- /dev/null +++ b/modules/sdk-coin-injective/.prettierignore @@ -0,0 +1,2 @@ +.nyc_output/ +dist/ diff --git a/modules/sdk-coin-injective/.prettierrc.yml b/modules/sdk-coin-injective/.prettierrc.yml new file mode 100644 index 0000000000..7c3d8dd32a --- /dev/null +++ b/modules/sdk-coin-injective/.prettierrc.yml @@ -0,0 +1,3 @@ +printWidth: 120 +singleQuote: true +trailingComma: 'es5' diff --git a/modules/sdk-coin-injective/README.md b/modules/sdk-coin-injective/README.md new file mode 100644 index 0000000000..f0400876fa --- /dev/null +++ b/modules/sdk-coin-injective/README.md @@ -0,0 +1,30 @@ +# BitGo sdk-coin-injective + +SDK coins provide a modular approach to a monolithic architecture. This and all BitGoJS SDK coins allow developers to use only the coins needed for a given project. + +## Installation + +All coins are loaded traditionally through the `bitgo` package. If you are using coins individually, you will be accessing the coin via the `@bitgo/sdk-api` package. + +In your project install both `@bitgo/sdk-api` and `@bitgo/sdk-coin-injective`. + +```shell +npm i @bitgo/sdk-api @bitgo/sdk-coin-injective +``` + +Next, you will be able to initialize an instance of "bitgo" through `@bitgo/sdk-api` instead of `bitgo`. + +```javascript +import { BitGoAPI } from '@bitgo/sdk-api'; +import { Injective } from '@bitgo/sdk-coin-injective'; + +const sdk = new BitGoAPI(); + +sdk.register('injective', Injective.createInstance); +``` + +## Development + +Most of the coin implementations are derived from `@bitgo/sdk-core`, `@bitgo/statics`, and coin specific packages. These implementations are used to interact with the BitGo API and BitGo platform services. + +You will notice that the basic version of common class extensions have been provided to you and must be resolved before the package build will succeed. Upon initiation of a given SDK coin, you will need to verify that your coin has been included in the root `tsconfig.packages.json` and that the linting, formatting, and testing succeeds when run both within the coin and from the root of BitGoJS. diff --git a/modules/sdk-coin-injective/package.json b/modules/sdk-coin-injective/package.json new file mode 100644 index 0000000000..a25fe4ac91 --- /dev/null +++ b/modules/sdk-coin-injective/package.json @@ -0,0 +1,56 @@ +{ + "name": "@bitgo/sdk-coin-injective", + "version": "1.1.2", + "description": "BitGo SDK coin library for Injective", + "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "scripts": { + "build": "yarn tsc --build --incremental --verbose .", + "fmt": "prettier --write .", + "check-fmt": "prettier --check .", + "clean": "rm -r ./dist", + "lint": "eslint --quiet .", + "prepare": "npm run build", + "test": "npm run coverage", + "coverage": "nyc -- npm run unit-test", + "unit-test": "mocha" + }, + "author": "BitGo SDK Team ", + "license": "MIT", + "engines": { + "node": ">=14 <17" + }, + "repository": { + "type": "git", + "url": "https://github.com/BitGo/BitGoJS.git", + "directory": "modules/sdk-coin-injective" + }, + "lint-staged": { + "*.{js,ts}": [ + "yarn prettier --write", + "yarn eslint --fix" + ] + }, + "publishConfig": { + "access": "public" + }, + "nyc": { + "extension": [ + ".ts" + ] + }, + "dependencies": { + "@bitgo/abstract-cosmos": "^1.3.0", + "@bitgo/sdk-core": "^8.10.0", + "@bitgo/statics": "^17.0.1", + "@cosmjs/amino": "^0.29.5", + "@cosmjs/encoding": "^0.29.5", + "@cosmjs/stargate": "^0.29.5", + "bignumber.js": "^9.1.1" + }, + "devDependencies": { + "@bitgo/sdk-api": "^1.11.2", + "@bitgo/sdk-test": "^1.2.27", + "@types/lodash": "^4.14.183" + } +} diff --git a/modules/sdk-coin-injective/src/index.ts b/modules/sdk-coin-injective/src/index.ts new file mode 100644 index 0000000000..1e7a9f121b --- /dev/null +++ b/modules/sdk-coin-injective/src/index.ts @@ -0,0 +1,4 @@ +export * from './injective'; +export * from './lib'; +export * from './register'; +export * from './tinjective'; diff --git a/modules/sdk-coin-injective/src/injective.ts b/modules/sdk-coin-injective/src/injective.ts new file mode 100644 index 0000000000..556f368a7d --- /dev/null +++ b/modules/sdk-coin-injective/src/injective.ts @@ -0,0 +1,38 @@ +import { CosmosCoin } from '@bitgo/abstract-cosmos'; +import { BaseCoin, BitGoBase, Environments } from '@bitgo/sdk-core'; +import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics'; + +import { TransactionBuilderFactory } from './lib'; +import utils from './lib/utils'; + +export class Injective extends CosmosCoin { + protected readonly _staticsCoin: Readonly; + protected constructor(bitgo: BitGoBase, staticsCoin?: Readonly) { + super(bitgo, staticsCoin); + + if (!staticsCoin) { + throw new Error('missing required constructor parameter staticsCoin'); + } + + this._staticsCoin = staticsCoin; + } + + static createInstance(bitgo: BitGoBase, staticsCoin?: Readonly): BaseCoin { + return new Injective(bitgo, staticsCoin); + } + + /** @inheritDoc **/ + getBuilder(): TransactionBuilderFactory { + return new TransactionBuilderFactory(coins.get(this.getChain())); + } + + /** @inheritDoc **/ + isValidAddress(address: string): boolean { + return utils.isValidAddress(address) || utils.isValidValidatorAddress(address); + } + + /** @inheritDoc **/ + protected getPublicNodeUrl(): string { + return Environments[this.bitgo.getEnv()].injNodeUrl; + } +} diff --git a/modules/sdk-coin-injective/src/lib/constants.ts b/modules/sdk-coin-injective/src/lib/constants.ts new file mode 100644 index 0000000000..7a13ced992 --- /dev/null +++ b/modules/sdk-coin-injective/src/lib/constants.ts @@ -0,0 +1,3 @@ +export const validDenoms = ['ninj', 'uinj', 'minj', 'inj']; +export const accountAddressRegex = /^(inj)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/; +export const validatorAddressRegex = /^(injvaloper)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']+)$/; diff --git a/modules/sdk-coin-injective/src/lib/index.ts b/modules/sdk-coin-injective/src/lib/index.ts new file mode 100644 index 0000000000..87cc346db5 --- /dev/null +++ b/modules/sdk-coin-injective/src/lib/index.ts @@ -0,0 +1,10 @@ +import * as Constants from './constants'; +import * as Utils from './utils'; + +export { + CosmosTransaction as Transaction, + CosmosTransactionBuilder as TransactionBuilder, +} from '@bitgo/abstract-cosmos'; +export { KeyPair } from './keyPair'; +export { TransactionBuilderFactory } from './transactionBuilderFactory'; +export { Constants, Utils }; diff --git a/modules/sdk-coin-injective/src/lib/keyPair.ts b/modules/sdk-coin-injective/src/lib/keyPair.ts new file mode 100644 index 0000000000..7c3e54e24c --- /dev/null +++ b/modules/sdk-coin-injective/src/lib/keyPair.ts @@ -0,0 +1,25 @@ +import { KeyPairOptions } from '@bitgo/sdk-core'; +import { pubkeyToAddress } from '@cosmjs/amino'; + +import { CosmosKeyPair } from '@bitgo/abstract-cosmos'; + +/** + * Injective keys and address management. + */ +export class KeyPair extends CosmosKeyPair { + constructor(source?: KeyPairOptions) { + super(source); + } + + /** @inheritdoc */ + getAddress(): string { + const base64String = Buffer.from(this.getKeys().pub.slice(0, 66), 'hex').toString('base64'); + return pubkeyToAddress( + { + type: 'tendermint/PubKeySecp256k1', + value: base64String, + }, + 'inj' + ); + } +} diff --git a/modules/sdk-coin-injective/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-injective/src/lib/transactionBuilderFactory.ts new file mode 100644 index 0000000000..9f5cd9b7a2 --- /dev/null +++ b/modules/sdk-coin-injective/src/lib/transactionBuilderFactory.ts @@ -0,0 +1,78 @@ +import { + CosmosTransaction, + CosmosTransactionBuilder, + CosmosTransferBuilder, + StakingActivateBuilder, + StakingDeactivateBuilder, + StakingWithdrawRewardsBuilder, +} from '@bitgo/abstract-cosmos'; +import { BaseTransactionBuilderFactory, InvalidTransactionError, TransactionType } from '@bitgo/sdk-core'; +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import injUtils from './utils'; + +export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { + constructor(_coinConfig: Readonly) { + super(_coinConfig); + } + + /** @inheritdoc */ + from(raw: string): CosmosTransactionBuilder { + const tx = new CosmosTransaction(this._coinConfig, injUtils); + tx.enrichTransactionDetailsFromRawTransaction(raw); + try { + switch (tx.type) { + case TransactionType.Send: + return this.getTransferBuilder(tx); + case TransactionType.StakingActivate: + return this.getStakingActivateBuilder(tx); + case TransactionType.StakingDeactivate: + return this.getStakingDeactivateBuilder(tx); + case TransactionType.StakingWithdraw: + return this.getStakingWithdrawRewardsBuilder(tx); + default: + throw new InvalidTransactionError('Invalid transaction'); + } + } catch (e) { + throw new InvalidTransactionError('Invalid transaction: ' + e.message); + } + } + + /** @inheritdoc */ + getTransferBuilder(tx?: CosmosTransaction): CosmosTransferBuilder { + return this.initializeBuilder(tx, new CosmosTransferBuilder(this._coinConfig, injUtils)); + } + + /** @inheritdoc */ + getStakingActivateBuilder(tx?: CosmosTransaction): StakingActivateBuilder { + return this.initializeBuilder(tx, new StakingActivateBuilder(this._coinConfig, injUtils)); + } + + /** @inheritdoc */ + getStakingDeactivateBuilder(tx?: CosmosTransaction): StakingDeactivateBuilder { + return this.initializeBuilder(tx, new StakingDeactivateBuilder(this._coinConfig, injUtils)); + } + + /** @inheritdoc */ + getStakingWithdrawRewardsBuilder(tx?: CosmosTransaction): StakingWithdrawRewardsBuilder { + return this.initializeBuilder(tx, new StakingWithdrawRewardsBuilder(this._coinConfig, injUtils)); + } + + /** @inheritdoc */ + getWalletInitializationBuilder(): void { + throw new Error('Method not implemented.'); + } + + /** + * Initialize the builder with the given transaction + * + * @param {CosmosTransaction | undefined} tx - the transaction used to initialize the builder + * @param {CosmosTransactionBuilder} builder - the builder to be initialized + * @returns {CosmosTransactionBuilder} the builder initialized + */ + protected initializeBuilder(tx: CosmosTransaction | undefined, builder: T): T { + if (tx) { + builder.initBuilder(tx); + } + return builder; + } +} diff --git a/modules/sdk-coin-injective/src/lib/utils.ts b/modules/sdk-coin-injective/src/lib/utils.ts new file mode 100644 index 0000000000..02ef141318 --- /dev/null +++ b/modules/sdk-coin-injective/src/lib/utils.ts @@ -0,0 +1,33 @@ +import { InvalidTransactionError } from '@bitgo/sdk-core'; +import { Coin } from '@cosmjs/stargate'; +import BigNumber from 'bignumber.js'; + +import { CosmosUtils } from '@bitgo/abstract-cosmos'; +import * as constants from './constants'; + +export class InjectiveUtils extends CosmosUtils { + /** @inheritdoc */ + isValidAddress(address: string): boolean { + return constants.accountAddressRegex.test(address); + } + + /** @inheritdoc */ + isValidValidatorAddress(address: string): boolean { + return constants.validatorAddressRegex.test(address); + } + + /** @inheritdoc */ + validateAmount(amount: Coin): void { + const amountBig = BigNumber(amount.amount); + if (amountBig.isLessThanOrEqualTo(0)) { + throw new InvalidTransactionError('transactionBuilder: validateAmount: Invalid amount: ' + amount.amount); + } + if (!constants.validDenoms.find((denom) => denom === amount.denom)) { + throw new InvalidTransactionError('transactionBuilder: validateAmount: Invalid denom: ' + amount.denom); + } + } +} + +const injUtils: CosmosUtils = new InjectiveUtils(); + +export default injUtils; diff --git a/modules/sdk-coin-injective/src/register.ts b/modules/sdk-coin-injective/src/register.ts new file mode 100644 index 0000000000..ab92da426e --- /dev/null +++ b/modules/sdk-coin-injective/src/register.ts @@ -0,0 +1,8 @@ +import { BitGoBase } from '@bitgo/sdk-core'; +import { Injective } from './injective'; +import { Tinjective } from './tinjective'; + +export const register = (sdk: BitGoBase): void => { + sdk.register('injective', Injective.createInstance); + sdk.register('tinjective', Tinjective.createInstance); +}; diff --git a/modules/sdk-coin-injective/src/tinjective.ts b/modules/sdk-coin-injective/src/tinjective.ts new file mode 100644 index 0000000000..b0fe91233b --- /dev/null +++ b/modules/sdk-coin-injective/src/tinjective.ts @@ -0,0 +1,25 @@ +/** + * Testnet Injective + * + * @format + */ +import { BaseCoin, BitGoBase } from '@bitgo/sdk-core'; +import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics'; +import { Injective } from './injective'; + +export class Tinjective extends Injective { + protected readonly _staticsCoin: Readonly; + protected constructor(bitgo: BitGoBase, staticsCoin?: Readonly) { + super(bitgo, staticsCoin); + + if (!staticsCoin) { + throw new Error('missing required constructor parameter staticsCoin'); + } + + this._staticsCoin = staticsCoin; + } + + static createInstance(bitgo: BitGoBase, staticsCoin?: Readonly): BaseCoin { + return new Tinjective(bitgo, staticsCoin); + } +} diff --git a/modules/sdk-coin-injective/test/integration/index.ts b/modules/sdk-coin-injective/test/integration/index.ts new file mode 100644 index 0000000000..54ed861898 --- /dev/null +++ b/modules/sdk-coin-injective/test/integration/index.ts @@ -0,0 +1,16 @@ +import 'should'; + +import { BitGoAPI } from '@bitgo/sdk-api'; +import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; + +import { Injective, Tinjective } from '../../src/index'; + +describe('INJ', function () { + let bitgo: TestBitGoAPI; + before(function () { + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); + bitgo.safeRegister('injective', Injective.createInstance); + bitgo.safeRegister('tinjective', Tinjective.createInstance); + bitgo.initializeTestVars(); + }); +}); diff --git a/modules/sdk-coin-injective/test/resources/injective.ts b/modules/sdk-coin-injective/test/resources/injective.ts new file mode 100644 index 0000000000..6258e6df63 --- /dev/null +++ b/modules/sdk-coin-injective/test/resources/injective.ts @@ -0,0 +1,228 @@ +// Get the test data by running the scripts for the particular coin from coin-sandbox repo. + +export const TEST_ACCOUNT = { + pubAddress: 'inj1vftzm2nnlt0x8z9gq2hgxxywlk79m8k7k3ljgk', + compressedPublicKey: '0332363e3dfdccd329423bb0cdc0aaebf9dc8c8d2acbedd21a77e232bb84c1c7dc', + compressedPublicKeyTwo: '03b0068d1db966573288f500e14d7fae04b5d083b7b500789e6eb03ce21413e6bc', + uncompressedPublicKey: + '0432363e3dfdccd329423bb0cdc0aaebf9dc8c8d2acbedd21a77e232bb84c1c7dc73c75bd6565b0dfaaad8ea4af5c9676a2e1ee0397dddcb633b56923daec4a2a3', + privateKey: '20753d58ed0e87834f154ce7e94c278875927d4d05227c1b892a9796aa3bc9dc', + extendedPrv: + 'xprv9s21ZrQH143K2P9CmgFZfiStbbt296pytKe7gUVpw1vvgcP13XxoJVHJ31CoSKGrFRBaQCbvFS5K1u1NXepG1WyBRyVw1faMh8C7StC25R9', + extendedPub: + 'xpub661MyMwAqRbcEsDfshna2rPd9diWYZYqFYZiUruSVMTuZQi9b5H3rHbmtJK8ZBxPz8oL6x4PegXqZ8Xsdn4akea8qY4GddQubaidVGsmQQW', +}; + +export const TEST_SEND_TX = { + hash: '12A20E2A5A2B54827A9B007D024D269E01957749E762706C3744BC76C56D4A81', + signature: 's1wGhKeNOhdn4gFDMxRP/tFbQ/fQS1vt99A5J7/dxaZONLN4+PFtFVA0Ey1iqvojSrMGeYZg4i0PyzHvK+54Vg==', + pubKey: 'AhvHHUQYuVrRbuUbrfX8NDqsQ/9swswreoiyzcg8L9bw', + privateKey: 'hrLO1X8Z5PVyDlBji5HjgrETVatKBmJhTZjg9Oq7b18=', + signedTxBase64: + 'CosBCogBChwvY29zbW9zLmJhbmsudjFiZXRhMS5Nc2dTZW5kEmgKKmluajE1cHo2ZnE2NTJjbWUwZ3g2ZGQyejl3dHphM3J3bXo5ajR5d2tzZBIqaW5qMWRybjN2emc2eGVwNjNwdGF4c3loajZweTVhajhmeTNkNDMwM3AwGg4KBHVpbmoSBjEwMDAwMBJwClAKRgofL2Nvc21vcy5jcnlwdG8uc2VjcDI1NmsxLlB1YktleRIjCiECG8cdRBi5WtFu5Rut9fw0OqxD/2zCzCt6iLLNyDwv1vASBAoCCAEYHhIcChYKA2luahIPMTAwMDAwMDAwMDAwMDAwEMCaDBpAs1wGhKeNOhdn4gFDMxRP/tFbQ/fQS1vt99A5J7/dxaZONLN4+PFtFVA0Ey1iqvojSrMGeYZg4i0PyzHvK+54Vg==', + sender: 'inj15pz6fq652cme0gx6dd2z9wtza3rwmz9j4ywksd', + recipient: 'inj1drn3vzg6xep63ptaxsyhj6py5aj8fy3d4303p0', + chainId: 'injective-888', + accountNumber: 13079, + sequence: 30, + sendAmount: '100000', + feeAmount: '100000000000000', + sendMessage: { + typeUrl: '/cosmos.bank.v1beta1.MsgSend', + value: { + amount: [ + { + denom: 'uinj', + amount: '100000', + }, + ], + toAddress: 'inj1drn3vzg6xep63ptaxsyhj6py5aj8fy3d4303p0', + fromAddress: 'inj15pz6fq652cme0gx6dd2z9wtza3rwmz9j4ywksd', + }, + }, + gasBudget: { + amount: [{ denom: 'inj', amount: '100000000000000' }], + gasLimit: 200000, + }, +}; + +export const TEST_DELEGATE_TX = { + hash: 'CFE83BCC9BDA9E4E51E3E03F772B7CAEFEE9E74E00F223B26C5AB3A8B7F3B44A', + signature: 'p5UoOHBw7mWPt4RA5ZNc9gOjcbIlYKx3/wz/cnHeTXVawu/kJ0Fr0TpfrTaZWvmstGaAUk1k5d5sM4b5ZIgIAw==', + pubKey: 'AhvHHUQYuVrRbuUbrfX8NDqsQ/9swswreoiyzcg8L9bw', + privateKey: 'hrLO1X8Z5PVyDlBji5HjgrETVatKBmJhTZjg9Oq7b18=', + signedTxBase64: + 'CpgBCpUBCiMvY29zbW9zLnN0YWtpbmcudjFiZXRhMS5Nc2dEZWxlZ2F0ZRJuCippbmoxNXB6NmZxNjUyY21lMGd4NmRkMno5d3R6YTNyd216OWo0eXdrc2QSMWluanZhbG9wZXIxa2s1MjNyc205cGV5NzQwY3g0cGxhbHA0MDAwOW5jczB3cmNoZmUaDQoEdWluahIFMTAwMDAScApQCkYKHy9jb3Ntb3MuY3J5cHRvLnNlY3AyNTZrMS5QdWJLZXkSIwohAhvHHUQYuVrRbuUbrfX8NDqsQ/9swswreoiyzcg8L9bwEgQKAggBGCASHAoWCgNpbmoSDzEwMDAwMDAwMDAwMDAwMBDAmgwaQKeVKDhwcO5lj7eEQOWTXPYDo3GyJWCsd/8M/3Jx3k11WsLv5CdBa9E6X602mVr5rLRmgFJNZOXebDOG+WSICAM=', + delegator: 'inj15pz6fq652cme0gx6dd2z9wtza3rwmz9j4ywksd', + validator: 'injvaloper1kk523rsm9pey740cx4plalp40009ncs0wrchfe', + chainId: 'injective-888', + accountNumber: 13079, + sequence: 32, + sendAmount: '10000', + feeAmount: '100000000000000', + sendMessage: { + typeUrl: '/cosmos.staking.v1beta1.MsgDelegate', + value: { + delegatorAddress: 'inj15pz6fq652cme0gx6dd2z9wtza3rwmz9j4ywksd', + validatorAddress: 'injvaloper1kk523rsm9pey740cx4plalp40009ncs0wrchfe', + amount: { + denom: 'uinj', + amount: '10000', + }, + }, + }, + gasBudget: { + amount: [ + { + denom: 'inj', + amount: '100000000000000', + }, + ], + gasLimit: 200000, + }, +}; + +export const TEST_UNDELEGATE_TX = { + hash: '2B373B66F359368527BDD9E40D716B3D66B46EDA048164BE00E655109A3FA374', + signature: '750kQqYHaQ22TUCX2O1TaGKiSeetFlUl7B4J3ZplohZrfRylW4K/MGNh1mQ7oisUD065o4howrvn0DcMtxWIdQ==', + pubKey: 'AhvHHUQYuVrRbuUbrfX8NDqsQ/9swswreoiyzcg8L9bw', + privateKey: 'hrLO1X8Z5PVyDlBji5HjgrETVatKBmJhTZjg9Oq7b18=', + signedTxBase64: + 'CpoBCpcBCiUvY29zbW9zLnN0YWtpbmcudjFiZXRhMS5Nc2dVbmRlbGVnYXRlEm4KKmluajE1cHo2ZnE2NTJjbWUwZ3g2ZGQyejl3dHphM3J3bXo5ajR5d2tzZBIxaW5qdmFsb3BlcjFrazUyM3JzbTlwZXk3NDBjeDRwbGFscDQwMDA5bmNzMHdyY2hmZRoNCgR1aW5qEgUxMDAwMBJwClAKRgofL2Nvc21vcy5jcnlwdG8uc2VjcDI1NmsxLlB1YktleRIjCiECG8cdRBi5WtFu5Rut9fw0OqxD/2zCzCt6iLLNyDwv1vASBAoCCAEYIRIcChYKA2luahIPMTAwMDAwMDAwMDAwMDAwEMCaDBpA750kQqYHaQ22TUCX2O1TaGKiSeetFlUl7B4J3ZplohZrfRylW4K/MGNh1mQ7oisUD065o4howrvn0DcMtxWIdQ==', + delegator: 'inj15pz6fq652cme0gx6dd2z9wtza3rwmz9j4ywksd', + validator: 'injvaloper1kk523rsm9pey740cx4plalp40009ncs0wrchfe', + chainId: 'injective-888', + accountNumber: 13079, + sequence: 33, + sendAmount: '10000', + feeAmount: '100000000000000', + sendMessage: { + typeUrl: '/cosmos.staking.v1beta1.MsgUndelegate', + value: { + delegatorAddress: 'inj15pz6fq652cme0gx6dd2z9wtza3rwmz9j4ywksd', + validatorAddress: 'injvaloper1kk523rsm9pey740cx4plalp40009ncs0wrchfe', + amount: { + denom: 'uinj', + amount: '10000', + }, + }, + }, + gasBudget: { + amount: [ + { + denom: 'inj', + amount: '100000000000000', + }, + ], + gasLimit: 200000, + }, +}; + +export const TEST_WITHDRAW_REWARDS_TX = { + hash: '55FBC9C1AC8674FFFAB1A80DAF928BC344DCBFE12876844C9E5FF08CD3FDE2F6', + signature: '3sAfy+B22YFY0iLLrJCBHvKYlYwtaUARBswcDUCfnP4mZPwkS7vI9CEEyg23AYahndYHxQG0W1IbSHeiNf909Q==', + pubKey: 'AhvHHUQYuVrRbuUbrfX8NDqsQ/9swswreoiyzcg8L9bw', + privateKey: 'hrLO1X8Z5PVyDlBji5HjgrETVatKBmJhTZjg9Oq7b18=', + signedTxBase64: + 'Cp0BCpoBCjcvY29zbW9zLmRpc3RyaWJ1dGlvbi52MWJldGExLk1zZ1dpdGhkcmF3RGVsZWdhdG9yUmV3YXJkEl8KKmluajE1cHo2ZnE2NTJjbWUwZ3g2ZGQyejl3dHphM3J3bXo5ajR5d2tzZBIxaW5qdmFsb3BlcjFrazUyM3JzbTlwZXk3NDBjeDRwbGFscDQwMDA5bmNzMHdyY2hmZRJwClAKRgofL2Nvc21vcy5jcnlwdG8uc2VjcDI1NmsxLlB1YktleRIjCiECG8cdRBi5WtFu5Rut9fw0OqxD/2zCzCt6iLLNyDwv1vASBAoCCAEYIhIcChYKA2luahIPMTAwMDAwMDAwMDAwMDAwEMCaDBpA3sAfy+B22YFY0iLLrJCBHvKYlYwtaUARBswcDUCfnP4mZPwkS7vI9CEEyg23AYahndYHxQG0W1IbSHeiNf909Q==', + delegator: 'inj15pz6fq652cme0gx6dd2z9wtza3rwmz9j4ywksd', + validator: 'injvaloper1kk523rsm9pey740cx4plalp40009ncs0wrchfe', + chainId: 'injective-888', + accountNumber: 13079, + sequence: 34, + sendAmount: '10000', + feeAmount: '100000000000000', + sendMessage: { + typeUrl: '/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward', + value: { + delegatorAddress: 'inj15pz6fq652cme0gx6dd2z9wtza3rwmz9j4ywksd', + validatorAddress: 'injvaloper1kk523rsm9pey740cx4plalp40009ncs0wrchfe', + amount: { + denom: 'uinj', + amount: '10000', + }, + }, + }, + gasBudget: { + amount: [ + { + denom: 'inj', + amount: '100000000000000', + }, + ], + gasLimit: 200000, + }, +}; + +export const TEST_TX_WITH_MEMO = { + hash: 'A9696BBAFE896A81DC4B339F3419361ECFE8EF6B6792FC2EA4F081962DF2B138', + signature: 'QzTMK2mw2/tVw2t4En+2V/C5T2Iy/41tJ216GLJPWCgd7USBdDRsXrROwbGoxYi71hMvs58/DEAUaM7f8uGk0w==', + pubKey: 'AhvHHUQYuVrRbuUbrfX8NDqsQ/9swswreoiyzcg8L9bw', + privateKey: 'hrLO1X8Z5PVyDlBji5HjgrETVatKBmJhTZjg9Oq7b18=', + signedTxBase64: + 'Co4BCogBChwvY29zbW9zLmJhbmsudjFiZXRhMS5Nc2dTZW5kEmgKKmluajE1cHo2ZnE2NTJjbWUwZ3g2ZGQyejl3dHphM3J3bXo5ajR5d2tzZBIqaW5qMWRybjN2emc2eGVwNjNwdGF4c3loajZweTVhajhmeTNkNDMwM3AwGg4KBHVpbmoSBjEwMDAwMBIBNRJwClAKRgofL2Nvc21vcy5jcnlwdG8uc2VjcDI1NmsxLlB1YktleRIjCiECG8cdRBi5WtFu5Rut9fw0OqxD/2zCzCt6iLLNyDwv1vASBAoCCAEYHxIcChYKA2luahIPMTAwMDAwMDAwMDAwMDAwEMCaDBpAQzTMK2mw2/tVw2t4En+2V/C5T2Iy/41tJ216GLJPWCgd7USBdDRsXrROwbGoxYi71hMvs58/DEAUaM7f8uGk0w==', + from: 'inj15pz6fq652cme0gx6dd2z9wtza3rwmz9j4ywksd', + to: 'inj1drn3vzg6xep63ptaxsyhj6py5aj8fy3d4303p0', + chainId: 'injective-888', + accountNumber: 13079, + sequence: 31, + sendAmount: '100000', + feeAmount: '100000000000000', + sendMessage: { + typeUrl: '/cosmos.bank.v1beta1.MsgSend', + value: { + amount: [ + { + denom: 'uinj', + amount: '100000', + }, + ], + toAddress: 'inj1drn3vzg6xep63ptaxsyhj6py5aj8fy3d4303p0', + fromAddress: 'inj15pz6fq652cme0gx6dd2z9wtza3rwmz9j4ywksd', + }, + }, + memo: '5', + gasBudget: { + amount: [ + { + denom: 'inj', + amount: '100000000000000', + }, + ], + gasLimit: 200000, + }, +}; + +export const address = { + address1: 'inj15pz6fq652cme0gx6dd2z9wtza3rwmz9j4ywksd', + address2: 'inj1drn3vzg6xep63ptaxsyhj6py5aj8fy3d4303p0', + address3: 'inx1xxn3vzg6xep63ptaxsyhj6py5aj8fy3d4303p0', + address4: 'inj1vftzm2nnlt0x8z9gq2hgxxywlk79m8k7k3ljgk', + validatorAddress1: 'injvaloper1kk523rsm9pey740cx4plalp40009ncs0wrchfe', + validatorAddress2: 'injvaloper15uad884tqeq9r76x3fvktmjge2r6kek55c2zpa', + validatorAddress3: 'inxvaloper1xxx23rsm9pey740cx4plalp40009ncs0wrchfe', + validatorAddress4: 'injvalopr1xkk523rsm9pey740cx4plalp40009ncs0wrchfe', + noMemoIdAddress: 'inj15uad884tqeq9r76x3fvktmjge2r6kek55c2zpa', + validMemoIdAddress: 'inj15uad884tqeq9r76x3fvktmjge2r6kek55c2zpa?memoId=2', + invalidMemoIdAddress: 'inj15uad884tqeq9r76x3fvktmjge2r6kek55c2zpa?memoId=xyz', + multipleMemoIdAddress: 'inj15uad884tqeq9r76x3fvktmjge2r6kek55c2zpa?memoId=3&memoId=12', +}; + +export const blockHash = { + hash1: 'AA8371F7D8796DDF24B660481B46C1F772135DA981750FF904DAC49C8282AB2A', + hash2: 'BF476AB6A3A29D1EF1C70B3ACE0E9F0F5E877DD0DABD0146BE8380B31900C696', +}; + +export const txIds = { + hash1: '7FCF872971836FCD571460898B05A6A1A1B6AB1E9C610B641E1D17FABA73B8F8', + hash2: 'E6F064F7E9EC4709F357E3030A86196E34830287B670FF590FAB85F1E1A30B2E', + hash3: '1184CF1119C7AF7BE49A13D10746ECAA133328E87B4CCFAA916C0B50EF646B41', +}; + +export const coinAmounts = { + amount1: { amount: '100000', denom: 'uinj' }, + amount2: { amount: '0.1', denom: 'inj' }, + amount3: { amount: '100000000000', denom: 'ninj' }, + amount4: { amount: '-1', denom: 'uinj' }, + amount5: { amount: '1000', denom: 'hinj' }, +}; diff --git a/modules/sdk-coin-injective/test/unit/injective.ts b/modules/sdk-coin-injective/test/unit/injective.ts new file mode 100644 index 0000000000..4a5b5b7487 --- /dev/null +++ b/modules/sdk-coin-injective/test/unit/injective.ts @@ -0,0 +1,339 @@ +import { BitGoAPI } from '@bitgo/sdk-api'; +import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; +import BigNumber from 'bignumber.js'; +import sinon from 'sinon'; +import { Injective, Tinjective } from '../../src'; +import utils from '../../src/lib/utils'; +import { + TEST_DELEGATE_TX, + TEST_SEND_TX, + TEST_TX_WITH_MEMO, + TEST_UNDELEGATE_TX, + TEST_WITHDRAW_REWARDS_TX, + address, +} from '../resources/injective'; +import should = require('should'); + +describe('INJ', function () { + let bitgo: TestBitGoAPI; + let basecoin; + before(function () { + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); + bitgo.safeRegister('injective', Injective.createInstance); + bitgo.safeRegister('tinjective', Tinjective.createInstance); + bitgo.initializeTestVars(); + basecoin = bitgo.coin('tinjective'); + }); + + it('should return the right info', function () { + const injective = bitgo.coin('injective'); + const tinjective = bitgo.coin('tinjective'); + + injective.getChain().should.equal('injective'); + injective.getFamily().should.equal('injective'); + injective.getFullName().should.equal('Injective'); + injective.getBaseFactor().should.equal(1e6); + + tinjective.getChain().should.equal('tinjective'); + tinjective.getFamily().should.equal('injective'); + tinjective.getFullName().should.equal('Testnet Injective'); + tinjective.getBaseFactor().should.equal(1e6); + }); + + describe('Address Validation', () => { + it('should get address details without memoId', function () { + const addressDetails = basecoin.getAddressDetails(address.noMemoIdAddress); + addressDetails.address.should.equal(address.noMemoIdAddress); + should.not.exist(addressDetails.memoId); + }); + + it('should get address details with memoId', function () { + const addressDetails = basecoin.getAddressDetails(address.validMemoIdAddress); + addressDetails.address.should.equal(address.validMemoIdAddress.split('?')[0]); + addressDetails.memoId.should.equal('2'); + }); + + it('should throw on invalid memo id address', () => { + (() => { + basecoin.getAddressDetails(address.invalidMemoIdAddress); + }).should.throw(); + }); + + it('should throw on multiple memo id address', () => { + (() => { + basecoin.getAddressDetails(address.multipleMemoIdAddress); + }).should.throw(); + }); + + it('should validate wallet receive address', async function () { + const receiveAddress = { + address: 'inj1vftzm2nnlt0x8z9gq2hgxxywlk79m8k7k3ljgk?memoId=7', + coinSpecific: { + rootAddress: 'inj1vftzm2nnlt0x8z9gq2hgxxywlk79m8k7k3ljgk', + memoID: '7', + }, + }; + const isValid = await basecoin.isWalletAddress(receiveAddress); + isValid.should.equal(true); + }); + + it('should validate account addresses correctly', () => { + should.equal(utils.isValidAddress(address.address1), true); + should.equal(utils.isValidAddress(address.address2), true); + should.equal(utils.isValidAddress(address.address3), false); + should.equal(utils.isValidAddress(address.address4), true); + should.equal(utils.isValidAddress('dfjk35y'), false); + should.equal(utils.isValidAddress(undefined as unknown as string), false); + should.equal(utils.isValidAddress(''), false); + }); + + it('should validate validator addresses correctly', () => { + should.equal(utils.isValidValidatorAddress(address.validatorAddress1), true); + should.equal(utils.isValidValidatorAddress(address.validatorAddress2), true); + should.equal(utils.isValidValidatorAddress(address.validatorAddress3), false); + should.equal(utils.isValidValidatorAddress(address.validatorAddress4), false); + should.equal(utils.isValidValidatorAddress('dfjk35y'), false); + should.equal(utils.isValidValidatorAddress(undefined as unknown as string), false); + should.equal(utils.isValidValidatorAddress(''), false); + }); + }); + + describe('Verify transaction: ', () => { + it('should succeed to verify transaction', async function () { + const txPrebuild = { + txHex: TEST_SEND_TX.signedTxBase64, + txInfo: {}, + }; + const txParams = { + recipients: [ + { + address: TEST_SEND_TX.recipient, + amount: TEST_SEND_TX.sendAmount, + }, + ], + }; + const verification = {}; + const isTransactionVerified = await basecoin.verifyTransaction({ txParams, txPrebuild, verification }); + isTransactionVerified.should.equal(true); + }); + + it('should succeed to verify delegate transaction', async function () { + const txPrebuild = { + txHex: TEST_DELEGATE_TX.signedTxBase64, + txInfo: {}, + }; + const txParams = { + recipients: [ + { + address: TEST_DELEGATE_TX.validator, + amount: TEST_DELEGATE_TX.sendAmount, + }, + ], + }; + const verification = {}; + const isTransactionVerified = await basecoin.verifyTransaction({ txParams, txPrebuild, verification }); + isTransactionVerified.should.equal(true); + }); + + it('should succeed to verify undelegate transaction', async function () { + const txPrebuild = { + txHex: TEST_UNDELEGATE_TX.signedTxBase64, + txInfo: {}, + }; + const txParams = { + recipients: [ + { + address: TEST_UNDELEGATE_TX.validator, + amount: TEST_UNDELEGATE_TX.sendAmount, + }, + ], + }; + const verification = {}; + const isTransactionVerified = await basecoin.verifyTransaction({ txParams, txPrebuild, verification }); + isTransactionVerified.should.equal(true); + }); + + it('should succeed to verify withdraw rewards transaction', async function () { + const txPrebuild = { + txHex: TEST_WITHDRAW_REWARDS_TX.signedTxBase64, + txInfo: {}, + }; + const txParams = { + recipients: [ + { + address: TEST_WITHDRAW_REWARDS_TX.validator, + amount: 'UNAVAILABLE', + }, + ], + }; + const verification = {}; + const isTransactionVerified = await basecoin.verifyTransaction({ txParams, txPrebuild, verification }); + isTransactionVerified.should.equal(true); + }); + + it('should fail to verify transaction with invalid param', async function () { + const txPrebuild = {}; + const txParams = { recipients: undefined }; + await basecoin + .verifyTransaction({ + txParams, + txPrebuild, + }) + .should.rejectedWith('missing required tx prebuild property txHex'); + }); + }); + + describe('Explain Transaction: ', () => { + it('should explain a transfer transaction', async function () { + const explainedTransaction = await basecoin.explainTransaction({ + txHex: TEST_SEND_TX.signedTxBase64, + }); + explainedTransaction.should.deepEqual({ + displayOrder: ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'type'], + id: TEST_SEND_TX.hash, + outputs: [ + { + address: TEST_SEND_TX.recipient, + amount: TEST_SEND_TX.sendAmount, + }, + ], + outputAmount: TEST_SEND_TX.sendAmount, + changeOutputs: [], + changeAmount: '0', + fee: { fee: TEST_SEND_TX.gasBudget.amount[0].amount }, + type: 0, + }); + }); + + it('should explain a delegate transaction', async function () { + const explainedTransaction = await basecoin.explainTransaction({ + txHex: TEST_DELEGATE_TX.signedTxBase64, + }); + explainedTransaction.should.deepEqual({ + displayOrder: ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'type'], + id: TEST_DELEGATE_TX.hash, + outputs: [ + { + address: TEST_DELEGATE_TX.validator, + amount: TEST_DELEGATE_TX.sendAmount, + }, + ], + outputAmount: TEST_DELEGATE_TX.sendAmount, + changeOutputs: [], + changeAmount: '0', + fee: { fee: TEST_DELEGATE_TX.gasBudget.amount[0].amount }, + type: 13, + }); + }); + + it('should explain a undelegate transaction', async function () { + const explainedTransaction = await basecoin.explainTransaction({ + txHex: TEST_UNDELEGATE_TX.signedTxBase64, + }); + explainedTransaction.should.deepEqual({ + displayOrder: ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'type'], + id: TEST_UNDELEGATE_TX.hash, + outputs: [ + { + address: TEST_UNDELEGATE_TX.validator, + amount: TEST_UNDELEGATE_TX.sendAmount, + }, + ], + outputAmount: TEST_UNDELEGATE_TX.sendAmount, + changeOutputs: [], + changeAmount: '0', + fee: { fee: TEST_UNDELEGATE_TX.gasBudget.amount[0].amount }, + type: 17, + }); + }); + + it('should explain a withdraw transaction', async function () { + const explainedTransaction = await basecoin.explainTransaction({ + txHex: TEST_WITHDRAW_REWARDS_TX.signedTxBase64, + }); + explainedTransaction.should.deepEqual({ + displayOrder: ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'type'], + id: TEST_WITHDRAW_REWARDS_TX.hash, + outputs: [ + { + address: TEST_WITHDRAW_REWARDS_TX.validator, + amount: 'UNAVAILABLE', + }, + ], + outputAmount: undefined, + changeOutputs: [], + changeAmount: '0', + fee: { fee: TEST_WITHDRAW_REWARDS_TX.gasBudget.amount[0].amount }, + type: 15, + }); + }); + + it('should explain a transfer transaction with memo', async function () { + const explainedTransaction = await basecoin.explainTransaction({ + txHex: TEST_TX_WITH_MEMO.signedTxBase64, + }); + explainedTransaction.should.deepEqual({ + displayOrder: ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'type'], + id: TEST_TX_WITH_MEMO.hash, + outputs: [ + { + address: TEST_TX_WITH_MEMO.to, + amount: TEST_TX_WITH_MEMO.sendAmount, + memo: TEST_TX_WITH_MEMO.memo, + }, + ], + outputAmount: TEST_TX_WITH_MEMO.sendAmount, + changeOutputs: [], + changeAmount: '0', + fee: { fee: TEST_TX_WITH_MEMO.gasBudget.amount[0].amount }, + type: 0, + }); + }); + + it('should fail to explain transaction with missing params', async function () { + try { + await basecoin.explainTransaction({}); + } catch (error) { + should.equal(error.message, 'missing required txHex parameter'); + } + }); + + it('should fail to explain transaction with invalid params', async function () { + try { + await basecoin.explainTransaction({ txHex: 'randomString' }); + } catch (error) { + should.equal(error.message.startsWith('Invalid transaction:'), true); + } + }); + }); + + describe('Parse Transactions: ', () => { + it('should parse a transfer transaction', async function () { + const transferInputsResponse = { + address: TEST_SEND_TX.recipient, + amount: new BigNumber(TEST_SEND_TX.sendAmount).plus(TEST_SEND_TX.gasBudget.amount[0].amount).toFixed(), + }; + + const transferOutputsResponse = { + address: TEST_SEND_TX.recipient, + amount: TEST_SEND_TX.sendAmount, + }; + + const parsedTransaction = await basecoin.parseTransaction({ txHex: TEST_SEND_TX.signedTxBase64 }); + + parsedTransaction.should.deepEqual({ + inputs: [transferInputsResponse], + outputs: [transferOutputsResponse], + }); + }); + + it('should fail to parse a transfer transaction when explainTransaction response is undefined', async function () { + const stub = sinon.stub(Injective.prototype, 'explainTransaction'); + stub.resolves(undefined); + await basecoin + .parseTransaction({ txHex: TEST_SEND_TX.signedTxBase64 }) + .should.be.rejectedWith('Invalid transaction'); + stub.restore(); + }); + }); +}); diff --git a/modules/sdk-coin-injective/test/unit/keyPair.ts b/modules/sdk-coin-injective/test/unit/keyPair.ts new file mode 100644 index 0000000000..c7399d6a98 --- /dev/null +++ b/modules/sdk-coin-injective/test/unit/keyPair.ts @@ -0,0 +1,117 @@ +import assert from 'assert'; +import should from 'should'; +import { KeyPair } from '../../src'; +import { TEST_ACCOUNT } from '../resources/injective'; + +describe('INJ Key Pair', () => { + describe('should create a valid KeyPair', () => { + it('from an empty value', () => { + const keyPairObj = new KeyPair(); + const keys = keyPairObj.getKeys(); + should.exists(keys.prv); + should.exists(keys.pub); + should.equal(keys.prv!.length, 64); + should.equal(keys.pub.length, 66); + + const extendedKeys = keyPairObj.getExtendedKeys(); + should.exists(extendedKeys.xprv); + should.exists(extendedKeys.xpub); + }); + + it('from a private key', () => { + const privateKey = TEST_ACCOUNT.privateKey; + const keyPairObj = new KeyPair({ prv: privateKey }); + const keys = keyPairObj.getKeys(); + should.exists(keys.prv); + should.exists(keys.pub); + should.equal(keys.prv, TEST_ACCOUNT.privateKey); + should.equal(keys.pub, TEST_ACCOUNT.compressedPublicKey); + should.equal(keyPairObj.getAddress(), TEST_ACCOUNT.pubAddress); + + assert.throws(() => keyPairObj.getExtendedKeys()); + }); + + it('from a compressed public key', () => { + const publicKey = TEST_ACCOUNT.compressedPublicKey; + const keyPairObj = new KeyPair({ pub: publicKey }); + const keys = keyPairObj.getKeys(); + should.not.exist(keys.prv); + should.exists(keys.pub); + should.equal(keys.pub, TEST_ACCOUNT.compressedPublicKey); + + assert.throws(() => keyPairObj.getExtendedKeys()); + }); + + it('from an uncompressed public key', () => { + // Input is uncompressed, but we output the compressed key to keep + // parity with Injective network expectations. + const publicKey = TEST_ACCOUNT.uncompressedPublicKey; + const keyPairObj = new KeyPair({ pub: publicKey }); + const keys = keyPairObj.getKeys(); + should.not.exist(keys.prv); + should.exists(keys.pub); + should.notEqual(keys.pub, publicKey); + should.equal(keys.pub, TEST_ACCOUNT.compressedPublicKey); + + assert.throws(() => keyPairObj.getExtendedKeys()); + }); + }); + + describe('should fail to create a KeyPair', () => { + it('from an invalid privateKey', () => { + assert.throws( + () => new KeyPair({ prv: '' }), + (e) => e.message === 'Unsupported private key' + ); + }); + + it('from an invalid publicKey', () => { + assert.throws( + () => new KeyPair({ pub: '' }), + (e) => e.message.startsWith('Unsupported public key') + ); + }); + + it('from an undefined seed', () => { + const undefinedBuffer = undefined as unknown as Buffer; + assert.throws( + () => new KeyPair({ seed: undefinedBuffer }), + (e) => e.message.startsWith('Invalid key pair options') + ); + }); + + it('from an undefined private key', () => { + const undefinedStr: string = undefined as unknown as string; + assert.throws( + () => new KeyPair({ prv: undefinedStr }), + (e) => e.message.startsWith('Invalid key pair options') + ); + }); + + it('from an undefined public key', () => { + const undefinedStr: string = undefined as unknown as string; + assert.throws( + () => new KeyPair({ pub: undefinedStr }), + (e) => e.message.startsWith('Invalid key pair options') + ); + }); + }); + + describe('should get unique address', () => { + it('from a private key', () => { + const keyPair = new KeyPair({ prv: TEST_ACCOUNT.privateKey }); + should.equal(keyPair.getAddress(), TEST_ACCOUNT.pubAddress); + }); + + it('from a compressed public key', () => { + const keyPair = new KeyPair({ pub: TEST_ACCOUNT.compressedPublicKey }); + should.equal(keyPair.getAddress(), TEST_ACCOUNT.pubAddress); + }); + + it('should be different for different public keys', () => { + const keyPairOne = new KeyPair({ pub: TEST_ACCOUNT.compressedPublicKey }); + const keyPairTwo = new KeyPair({ pub: TEST_ACCOUNT.compressedPublicKeyTwo }); + should.notEqual(keyPairOne.getAddress(), keyPairTwo.getAddress()); + }); + }); +}); diff --git a/modules/sdk-coin-injective/test/unit/transaction.ts b/modules/sdk-coin-injective/test/unit/transaction.ts new file mode 100644 index 0000000000..c2e338a816 --- /dev/null +++ b/modules/sdk-coin-injective/test/unit/transaction.ts @@ -0,0 +1,232 @@ +import { toHex, TransactionType } from '@bitgo/sdk-core'; +import { coins } from '@bitgo/statics'; +import { fromBase64 } from '@cosmjs/encoding'; +import should from 'should'; + +import { + CosmosTransaction, + DelegateOrUndelegeteMessage, + SendMessage, + WithdrawDelegatorRewardsMessage, +} from '@bitgo/abstract-cosmos'; +import utils from '../../src/lib/utils'; +import * as testData from '../resources/injective'; + +describe('Injective Transaction', () => { + let tx: CosmosTransaction; + const config = coins.get('tinjective'); + + beforeEach(() => { + tx = new CosmosTransaction(config, utils); + }); + + describe('Empty transaction', () => { + it('should throw empty transaction', function () { + should.throws(() => tx.toBroadcastFormat(), 'Empty transaction'); + }); + }); + + describe('From raw transaction', () => { + it('should build a transfer from raw signed base64', function () { + tx.enrichTransactionDetailsFromRawTransaction(testData.TEST_SEND_TX.signedTxBase64); + const json = tx.toJson(); + should.equal(json.sequence, testData.TEST_SEND_TX.sequence); + should.deepEqual(json.gasBudget, testData.TEST_SEND_TX.gasBudget); + should.equal(json.publicKey, toHex(fromBase64(testData.TEST_SEND_TX.pubKey))); + should.equal( + (json.sendMessages[0].value as SendMessage).toAddress, + testData.TEST_SEND_TX.sendMessage.value.toAddress + ); + should.deepEqual( + (json.sendMessages[0].value as SendMessage).amount, + testData.TEST_SEND_TX.sendMessage.value.amount + ); + should.equal(Buffer.from(json.signature as any).toString('base64'), testData.TEST_SEND_TX.signature); + should.equal(tx.type, TransactionType.Send); + tx.loadInputsAndOutputs(); + should.deepEqual(tx.inputs, [ + { + address: testData.TEST_SEND_TX.sender, + value: testData.TEST_SEND_TX.sendMessage.value.amount[0].amount, + coin: 'tinjective', + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.TEST_SEND_TX.sendMessage.value.toAddress, + value: testData.TEST_SEND_TX.sendMessage.value.amount[0].amount, + coin: 'tinjective', + }, + ]); + }); + + it('should build a transfer from raw signed hex', function () { + tx.enrichTransactionDetailsFromRawTransaction(toHex(fromBase64(testData.TEST_SEND_TX.signedTxBase64))); + const json = tx.toJson(); + should.equal(json.sequence, testData.TEST_SEND_TX.sequence); + should.deepEqual(json.gasBudget, testData.TEST_SEND_TX.gasBudget); + should.equal(json.publicKey, toHex(fromBase64(testData.TEST_SEND_TX.pubKey))); + should.equal( + (json.sendMessages[0].value as SendMessage).toAddress, + testData.TEST_SEND_TX.sendMessage.value.toAddress + ); + should.deepEqual( + (json.sendMessages[0].value as SendMessage).amount, + testData.TEST_SEND_TX.sendMessage.value.amount + ); + should.equal(Buffer.from(json.signature as any).toString('base64'), testData.TEST_SEND_TX.signature); + should.equal(tx.type, TransactionType.Send); + tx.loadInputsAndOutputs(); + should.deepEqual(tx.inputs, [ + { + address: testData.TEST_SEND_TX.sender, + value: testData.TEST_SEND_TX.sendMessage.value.amount[0].amount, + coin: 'tinjective', + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.TEST_SEND_TX.sendMessage.value.toAddress, + value: testData.TEST_SEND_TX.sendMessage.value.amount[0].amount, + coin: 'tinjective', + }, + ]); + }); + + it('should build a delegate txn from raw signed base64', function () { + tx.enrichTransactionDetailsFromRawTransaction(testData.TEST_DELEGATE_TX.signedTxBase64); + const json = tx.toJson(); + should.equal(json.sequence, testData.TEST_DELEGATE_TX.sequence); + should.deepEqual(json.gasBudget, testData.TEST_DELEGATE_TX.gasBudget); + should.equal(Buffer.from(json.publicKey as any, 'hex').toString('base64'), testData.TEST_DELEGATE_TX.pubKey); + should.equal( + (json.sendMessages[0].value as DelegateOrUndelegeteMessage).validatorAddress, + testData.TEST_DELEGATE_TX.sendMessage.value.validatorAddress + ); + should.deepEqual( + (json.sendMessages[0].value as DelegateOrUndelegeteMessage).amount, + testData.TEST_DELEGATE_TX.sendMessage.value.amount + ); + should.equal(Buffer.from(json.signature as any).toString('base64'), testData.TEST_DELEGATE_TX.signature); + should.equal(tx.type, TransactionType.StakingActivate); + tx.loadInputsAndOutputs(); + should.deepEqual(tx.inputs, [ + { + address: testData.TEST_DELEGATE_TX.delegator, + value: testData.TEST_DELEGATE_TX.sendMessage.value.amount.amount, + coin: 'tinjective', + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.TEST_DELEGATE_TX.validator, + value: testData.TEST_DELEGATE_TX.sendMessage.value.amount.amount, + coin: 'tinjective', + }, + ]); + }); + + it('should build a undelegate txn from raw signed base64', function () { + tx.enrichTransactionDetailsFromRawTransaction(testData.TEST_UNDELEGATE_TX.signedTxBase64); + const json = tx.toJson(); + should.equal(json.sequence, testData.TEST_UNDELEGATE_TX.sequence); + should.deepEqual(json.gasBudget, testData.TEST_UNDELEGATE_TX.gasBudget); + should.equal(Buffer.from(json.publicKey as any, 'hex').toString('base64'), testData.TEST_UNDELEGATE_TX.pubKey); + should.equal( + (json.sendMessages[0].value as DelegateOrUndelegeteMessage).validatorAddress, + testData.TEST_UNDELEGATE_TX.sendMessage.value.validatorAddress + ); + should.deepEqual( + (json.sendMessages[0].value as DelegateOrUndelegeteMessage).amount, + testData.TEST_UNDELEGATE_TX.sendMessage.value.amount + ); + should.equal(Buffer.from(json.signature as any).toString('base64'), testData.TEST_UNDELEGATE_TX.signature); + should.equal(tx.type, TransactionType.StakingDeactivate); + tx.loadInputsAndOutputs(); + should.deepEqual(tx.inputs, [ + { + address: testData.TEST_UNDELEGATE_TX.delegator, + value: testData.TEST_UNDELEGATE_TX.sendMessage.value.amount.amount, + coin: 'tinjective', + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.TEST_UNDELEGATE_TX.validator, + value: testData.TEST_UNDELEGATE_TX.sendMessage.value.amount.amount, + coin: 'tinjective', + }, + ]); + }); + + it('should build a withdraw rewards from raw signed base64', function () { + tx.enrichTransactionDetailsFromRawTransaction(testData.TEST_WITHDRAW_REWARDS_TX.signedTxBase64); + const json = tx.toJson(); + should.equal(json.sequence, testData.TEST_WITHDRAW_REWARDS_TX.sequence); + should.deepEqual(json.gasBudget, testData.TEST_WITHDRAW_REWARDS_TX.gasBudget); + should.equal( + Buffer.from(json.publicKey as any, 'hex').toString('base64'), + testData.TEST_WITHDRAW_REWARDS_TX.pubKey + ); + should.equal( + (json.sendMessages[0].value as WithdrawDelegatorRewardsMessage).validatorAddress, + testData.TEST_WITHDRAW_REWARDS_TX.sendMessage.value.validatorAddress + ); + should.equal(Buffer.from(json.signature as any).toString('base64'), testData.TEST_WITHDRAW_REWARDS_TX.signature); + should.equal(tx.type, TransactionType.StakingWithdraw); + + tx.loadInputsAndOutputs(); + should.deepEqual(tx.inputs, [ + { + address: testData.TEST_WITHDRAW_REWARDS_TX.delegator, + value: 'UNAVAILABLE', + coin: 'tinjective', + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.TEST_WITHDRAW_REWARDS_TX.validator, + value: 'UNAVAILABLE', + coin: 'tinjective', + }, + ]); + }); + + it('should fail to build a transfer from incorrect raw hex', function () { + should.throws( + () => tx.enrichTransactionDetailsFromRawTransaction('random' + testData.TEST_SEND_TX.signedTxBase64), + 'incorrect raw data' + ); + }); + + it('should fail to explain transaction with invalid raw hex', function () { + should.throws(() => tx.enrichTransactionDetailsFromRawTransaction('randomString'), 'Invalid transaction'); + }); + }); + + describe('Explain transaction', () => { + it('should explain a transfer pay transaction', function () { + tx.enrichTransactionDetailsFromRawTransaction(testData.TEST_SEND_TX.signedTxBase64); + const explainedTransaction = tx.explainTransaction(); + explainedTransaction.should.deepEqual({ + displayOrder: ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'type'], + id: testData.TEST_SEND_TX.hash, + outputs: [ + { + address: testData.TEST_SEND_TX.recipient, + amount: testData.TEST_SEND_TX.sendAmount, + }, + ], + outputAmount: testData.TEST_SEND_TX.sendAmount, + changeOutputs: [], + changeAmount: '0', + fee: { fee: testData.TEST_SEND_TX.feeAmount }, + type: 0, + }); + }); + + it('should fail to explain transaction with invalid raw base64 string', function () { + should.throws(() => tx.enrichTransactionDetailsFromRawTransaction('randomString'), 'Invalid transaction'); + }); + }); +}); diff --git a/modules/sdk-coin-injective/test/unit/transactionBuilder/StakingActivateBuilder.ts b/modules/sdk-coin-injective/test/unit/transactionBuilder/StakingActivateBuilder.ts new file mode 100644 index 0000000000..c3ab8f16a3 --- /dev/null +++ b/modules/sdk-coin-injective/test/unit/transactionBuilder/StakingActivateBuilder.ts @@ -0,0 +1,121 @@ +import { toHex, TransactionType } from '@bitgo/sdk-core'; +import { fromBase64 } from '@cosmjs/encoding'; +import should from 'should'; + +import { BitGoAPI } from '@bitgo/sdk-api'; +import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; +import { Injective, Tinjective } from '../../../src'; +import * as testData from '../../resources/injective'; + +describe('Injective Delegate txn Builder', () => { + let bitgo: TestBitGoAPI; + let basecoin; + let factory; + let testTx; + before(function () { + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); + bitgo.safeRegister('injective', Injective.createInstance); + bitgo.safeRegister('tinjective', Tinjective.createInstance); + bitgo.initializeTestVars(); + basecoin = bitgo.coin('tinjective'); + factory = basecoin.getBuilder(); + testTx = testData.TEST_DELEGATE_TX; + }); + + it('should build a Delegate tx with signature', async function () { + const txBuilder = factory.getStakingActivateBuilder(); + txBuilder.sequence(testTx.sequence); + txBuilder.gasBudget(testTx.gasBudget); + txBuilder.messages([testTx.sendMessage.value]); + txBuilder.publicKey(toHex(fromBase64(testTx.pubKey))); + txBuilder.addSignature({ pub: toHex(fromBase64(testTx.pubKey)) }, Buffer.from(testTx.signature, 'base64')); + + const tx = await txBuilder.build(); + const json = await (await txBuilder.build()).toJson(); + should.equal(tx.type, TransactionType.StakingActivate); + should.deepEqual(json.gasBudget, testTx.gasBudget); + should.deepEqual(json.sendMessages, [testTx.sendMessage]); + should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey))); + should.deepEqual(json.sequence, testTx.sequence); + const rawTx = tx.toBroadcastFormat(); + should.equal(rawTx, testTx.signedTxBase64); + should.deepEqual(tx.inputs, [ + { + address: testTx.delegator, + value: testTx.sendMessage.value.amount.amount, + coin: basecoin.getChain(), + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testTx.validator, + value: testTx.sendMessage.value.amount.amount, + coin: basecoin.getChain(), + }, + ]); + }); + + it('should build a Delegate tx without signature', async function () { + const txBuilder = factory.getStakingActivateBuilder(); + txBuilder.sequence(testTx.sequence); + txBuilder.gasBudget(testTx.gasBudget); + txBuilder.messages([testTx.sendMessage.value]); + txBuilder.publicKey(toHex(fromBase64(testTx.pubKey))); + const tx = await txBuilder.build(); + const json = await (await txBuilder.build()).toJson(); + should.equal(tx.type, TransactionType.StakingActivate); + should.deepEqual(json.gasBudget, testTx.gasBudget); + should.deepEqual(json.sendMessages, [testTx.sendMessage]); + should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey))); + should.deepEqual(json.sequence, testTx.sequence); + tx.toBroadcastFormat(); + should.deepEqual(tx.inputs, [ + { + address: testTx.delegator, + value: testTx.sendMessage.value.amount.amount, + coin: basecoin.getChain(), + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testTx.validator, + value: testTx.sendMessage.value.amount.amount, + coin: basecoin.getChain(), + }, + ]); + }); + + it('should sign a Delegate tx', async function () { + const txBuilder = factory.getStakingActivateBuilder(); + txBuilder.sequence(testTx.sequence); + txBuilder.gasBudget(testTx.gasBudget); + txBuilder.messages([testTx.sendMessage.value]); + txBuilder.accountNumber(testTx.accountNumber); + txBuilder.chainId(testTx.chainId); + txBuilder.sign({ key: toHex(fromBase64(testTx.privateKey)) }); + const tx = await txBuilder.build(); + const json = await (await txBuilder.build()).toJson(); + should.equal(tx.type, TransactionType.StakingActivate); + should.deepEqual(json.gasBudget, testTx.gasBudget); + should.deepEqual(json.sendMessages, [testTx.sendMessage]); + should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey))); + should.deepEqual(json.sequence, testTx.sequence); + const rawTx = tx.toBroadcastFormat(); + should.equal(tx.signature[0], toHex(fromBase64(testTx.signature))); + should.equal(rawTx, testTx.signedTxBase64); + should.deepEqual(tx.inputs, [ + { + address: testTx.delegator, + value: testTx.sendMessage.value.amount.amount, + coin: basecoin.getChain(), + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testTx.validator, + value: testTx.sendMessage.value.amount.amount, + coin: basecoin.getChain(), + }, + ]); + }); +}); diff --git a/modules/sdk-coin-injective/test/unit/transactionBuilder/StakingDeactivateBuilder.ts b/modules/sdk-coin-injective/test/unit/transactionBuilder/StakingDeactivateBuilder.ts new file mode 100644 index 0000000000..94a1a77d9d --- /dev/null +++ b/modules/sdk-coin-injective/test/unit/transactionBuilder/StakingDeactivateBuilder.ts @@ -0,0 +1,119 @@ +import { toHex, TransactionType } from '@bitgo/sdk-core'; +import { fromBase64 } from '@cosmjs/encoding'; +import should from 'should'; + +import { BitGoAPI } from '@bitgo/sdk-api'; +import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; +import { Injective, Tinjective } from '../../../src'; +import * as testData from '../../resources/injective'; + +describe('Injective Undelegate txn Builder', () => { + let bitgo: TestBitGoAPI; + let basecoin; + let factory; + let testTx; + before(function () { + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); + bitgo.safeRegister('injective', Injective.createInstance); + bitgo.safeRegister('tinjective', Tinjective.createInstance); + bitgo.initializeTestVars(); + basecoin = bitgo.coin('tinjective'); + factory = basecoin.getBuilder(); + testTx = testData.TEST_UNDELEGATE_TX; + }); + + it('should build undelegate tx with signature', async function () { + const txBuilder = factory.getStakingDeactivateBuilder(); + txBuilder.sequence(testTx.sequence); + txBuilder.gasBudget(testTx.gasBudget); + txBuilder.messages([testTx.sendMessage.value]); + txBuilder.addSignature({ pub: toHex(fromBase64(testTx.pubKey)) }, Buffer.from(testTx.signature, 'base64')); + const tx = await txBuilder.build(); + const json = await (await txBuilder.build()).toJson(); + should.equal(tx.type, TransactionType.StakingDeactivate); + should.deepEqual(json.gasBudget, testTx.gasBudget); + should.deepEqual(json.sendMessages, [testTx.sendMessage]); + should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey))); + should.deepEqual(json.sequence, testTx.sequence); + const rawTx = tx.toBroadcastFormat(); + should.equal(rawTx, testTx.signedTxBase64); + should.deepEqual(tx.inputs, [ + { + address: testTx.delegator, + value: testTx.sendMessage.value.amount.amount, + coin: basecoin.getChain(), + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testTx.validator, + value: testTx.sendMessage.value.amount.amount, + coin: basecoin.getChain(), + }, + ]); + }); + + it('should build undelegate tx without signature', async function () { + const txBuilder = factory.getStakingDeactivateBuilder(); + txBuilder.sequence(testTx.sequence); + txBuilder.gasBudget(testTx.gasBudget); + txBuilder.messages([testTx.sendMessage.value]); + txBuilder.publicKey(toHex(fromBase64(testTx.pubKey))); + const tx = await txBuilder.build(); + const json = await (await txBuilder.build()).toJson(); + should.equal(tx.type, TransactionType.StakingDeactivate); + should.deepEqual(json.gasBudget, testTx.gasBudget); + should.deepEqual(json.sendMessages, [testTx.sendMessage]); + should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey))); + should.deepEqual(json.sequence, testTx.sequence); + tx.toBroadcastFormat(); + should.deepEqual(tx.inputs, [ + { + address: testTx.delegator, + value: testTx.sendMessage.value.amount.amount, + coin: basecoin.getChain(), + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testTx.validator, + value: testTx.sendMessage.value.amount.amount, + coin: basecoin.getChain(), + }, + ]); + }); + + it('should sign undelegate tx', async function () { + const txBuilder = factory.getStakingDeactivateBuilder(); + txBuilder.sequence(testTx.sequence); + txBuilder.gasBudget(testTx.gasBudget); + txBuilder.messages([testTx.sendMessage.value]); + txBuilder.accountNumber(testTx.accountNumber); + txBuilder.chainId(testTx.chainId); + txBuilder.sign({ key: toHex(fromBase64(testTx.privateKey)) }); + const tx = await txBuilder.build(); + const json = await (await txBuilder.build()).toJson(); + should.equal(tx.type, TransactionType.StakingDeactivate); + should.deepEqual(json.gasBudget, testTx.gasBudget); + should.deepEqual(json.sendMessages, [testTx.sendMessage]); + should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey))); + should.deepEqual(json.sequence, testTx.sequence); + const rawTx = tx.toBroadcastFormat(); + should.equal(tx.signature[0], toHex(fromBase64(testTx.signature))); + should.equal(rawTx, testTx.signedTxBase64); + should.deepEqual(tx.inputs, [ + { + address: testTx.delegator, + value: testTx.sendMessage.value.amount.amount, + coin: basecoin.getChain(), + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testTx.validator, + value: testTx.sendMessage.value.amount.amount, + coin: basecoin.getChain(), + }, + ]); + }); +}); diff --git a/modules/sdk-coin-injective/test/unit/transactionBuilder/StakingWithdrawRewardsBuilder.ts b/modules/sdk-coin-injective/test/unit/transactionBuilder/StakingWithdrawRewardsBuilder.ts new file mode 100644 index 0000000000..a038bc1ed7 --- /dev/null +++ b/modules/sdk-coin-injective/test/unit/transactionBuilder/StakingWithdrawRewardsBuilder.ts @@ -0,0 +1,121 @@ +import { toHex, TransactionType } from '@bitgo/sdk-core'; +import { fromBase64 } from '@cosmjs/encoding'; +import should from 'should'; + +import { BitGoAPI } from '@bitgo/sdk-api'; +import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; +import { Injective, Tinjective } from '../../../src'; +import * as testData from '../../resources/injective'; + +describe('Injective WithdrawRewards txn Builder', () => { + let bitgo: TestBitGoAPI; + let basecoin; + let factory; + let testTx; + before(function () { + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); + bitgo.safeRegister('injective', Injective.createInstance); + bitgo.safeRegister('tinjective', Tinjective.createInstance); + bitgo.initializeTestVars(); + basecoin = bitgo.coin('tinjective'); + factory = basecoin.getBuilder(); + testTx = testData.TEST_WITHDRAW_REWARDS_TX; + }); + + it('should build a WithdrawRewards tx with signature', async function () { + const txBuilder = factory.getStakingWithdrawRewardsBuilder(); + txBuilder.sequence(testTx.sequence); + txBuilder.gasBudget(testTx.gasBudget); + txBuilder.messages([testTx.sendMessage.value]); + txBuilder.publicKey(toHex(fromBase64(testTx.pubKey))); + txBuilder.addSignature({ pub: toHex(fromBase64(testTx.pubKey)) }, Buffer.from(testTx.signature, 'base64')); + + const tx = await txBuilder.build(); + const json = await (await txBuilder.build()).toJson(); + should.equal(tx.type, TransactionType.StakingWithdraw); + should.deepEqual(json.gasBudget, testTx.gasBudget); + should.deepEqual(json.sendMessages, [testTx.sendMessage]); + should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey))); + should.deepEqual(json.sequence, testTx.sequence); + const rawTx = tx.toBroadcastFormat(); + should.equal(rawTx, testTx.signedTxBase64); + should.deepEqual(tx.inputs, [ + { + address: testData.TEST_WITHDRAW_REWARDS_TX.delegator, + value: 'UNAVAILABLE', + coin: basecoin.getChain(), + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.TEST_WITHDRAW_REWARDS_TX.validator, + value: 'UNAVAILABLE', + coin: basecoin.getChain(), + }, + ]); + }); + + it('should build a WithdrawRewards tx without signature', async function () { + const txBuilder = factory.getStakingWithdrawRewardsBuilder(); + txBuilder.sequence(testTx.sequence); + txBuilder.gasBudget(testTx.gasBudget); + txBuilder.messages([testTx.sendMessage.value]); + txBuilder.publicKey(toHex(fromBase64(testTx.pubKey))); + const tx = await txBuilder.build(); + const json = await (await txBuilder.build()).toJson(); + should.equal(tx.type, TransactionType.StakingWithdraw); + should.deepEqual(json.gasBudget, testTx.gasBudget); + should.deepEqual(json.sendMessages, [testTx.sendMessage]); + should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey))); + should.deepEqual(json.sequence, testTx.sequence); + tx.toBroadcastFormat(); + should.deepEqual(tx.inputs, [ + { + address: testData.TEST_WITHDRAW_REWARDS_TX.delegator, + value: 'UNAVAILABLE', + coin: basecoin.getChain(), + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.TEST_WITHDRAW_REWARDS_TX.validator, + value: 'UNAVAILABLE', + coin: basecoin.getChain(), + }, + ]); + }); + + it('should sign a WithdrawRewards tx', async function () { + const txBuilder = factory.getStakingWithdrawRewardsBuilder(); + txBuilder.sequence(testTx.sequence); + txBuilder.gasBudget(testTx.gasBudget); + txBuilder.messages([testTx.sendMessage.value]); + txBuilder.accountNumber(testTx.accountNumber); + txBuilder.chainId(testTx.chainId); + txBuilder.sign({ key: toHex(fromBase64(testTx.privateKey)) }); + const tx = await txBuilder.build(); + const json = await (await txBuilder.build()).toJson(); + should.equal(tx.type, TransactionType.StakingWithdraw); + should.deepEqual(json.gasBudget, testTx.gasBudget); + should.deepEqual(json.sendMessages, [testTx.sendMessage]); + should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey))); + should.deepEqual(json.sequence, testTx.sequence); + const rawTx = tx.toBroadcastFormat(); + should.equal(tx.signature[0], toHex(fromBase64(testTx.signature))); + should.equal(rawTx, testTx.signedTxBase64); + should.deepEqual(tx.inputs, [ + { + address: testData.TEST_WITHDRAW_REWARDS_TX.delegator, + value: 'UNAVAILABLE', + coin: basecoin.getChain(), + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.TEST_WITHDRAW_REWARDS_TX.validator, + value: 'UNAVAILABLE', + coin: basecoin.getChain(), + }, + ]); + }); +}); diff --git a/modules/sdk-coin-injective/test/unit/transactionBuilder/transactionBuilder.ts b/modules/sdk-coin-injective/test/unit/transactionBuilder/transactionBuilder.ts new file mode 100644 index 0000000000..45181e528e --- /dev/null +++ b/modules/sdk-coin-injective/test/unit/transactionBuilder/transactionBuilder.ts @@ -0,0 +1,83 @@ +import { TransactionType } from '@bitgo/sdk-core'; +import should from 'should'; + +import { BitGoAPI } from '@bitgo/sdk-api'; +import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; +import { Injective, Tinjective } from '../../../src'; +import * as testData from '../../resources/injective'; + +describe('Injective Transaction Builder', async () => { + let bitgo: TestBitGoAPI; + let basecoin; + let factory; + before(function () { + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); + bitgo.safeRegister('injective', Injective.createInstance); + bitgo.safeRegister('tinjective', Tinjective.createInstance); + bitgo.initializeTestVars(); + basecoin = bitgo.coin('tinjective'); + factory = basecoin.getBuilder(); + }); + + const testTxData = testData.TEST_SEND_TX; + let data; + + beforeEach(() => { + data = [ + { + type: TransactionType.Send, + testTx: testData.TEST_SEND_TX, + builder: factory.getTransferBuilder(), + }, + { + type: TransactionType.StakingActivate, + testTx: testData.TEST_DELEGATE_TX, + builder: factory.getStakingActivateBuilder(), + }, + { + type: TransactionType.StakingDeactivate, + testTx: testData.TEST_UNDELEGATE_TX, + builder: factory.getStakingDeactivateBuilder(), + }, + { + type: TransactionType.StakingWithdraw, + testTx: testData.TEST_WITHDRAW_REWARDS_TX, + builder: factory.getStakingWithdrawRewardsBuilder(), + }, + ]; + }); + + it('should build a signed tx from signed tx data', async function () { + const txBuilder = factory.from(testTxData.signedTxBase64); + const tx = await txBuilder.build(); + should.equal(tx.type, TransactionType.Send); + // Should recreate the same raw tx data when re-build and turned to broadcast format + const rawTx = tx.toBroadcastFormat(); + should.equal(rawTx, testTxData.signedTxBase64); + }); + + describe('gasBudget tests', async () => { + it('should succeed for valid gasBudget', function () { + for (const { builder } of data) { + should.doesNotThrow(() => builder.gasBudget(testTxData.gasBudget)); + } + }); + + it('should throw for invalid gasBudget', function () { + const invalidGasBudget = 0; + for (const { builder } of data) { + should(() => builder.gasBudget({ gasLimit: invalidGasBudget })).throw('Invalid gas limit ' + invalidGasBudget); + } + }); + }); + + it('validateAddress', function () { + const invalidAddress = { address: 'randomString' }; + for (const { builder } of data) { + should.doesNotThrow(() => builder.validateAddress({ address: testTxData.sender })); + should(() => builder.validateAddress(invalidAddress)).throwError( + 'transactionBuilder: address isValidAddress check failed: ' + invalidAddress.address + ); + } + }); +}); diff --git a/modules/sdk-coin-injective/test/unit/transactionBuilder/transferBuilder.ts b/modules/sdk-coin-injective/test/unit/transactionBuilder/transferBuilder.ts new file mode 100644 index 0000000000..b046d89357 --- /dev/null +++ b/modules/sdk-coin-injective/test/unit/transactionBuilder/transferBuilder.ts @@ -0,0 +1,160 @@ +import { BitGoAPI } from '@bitgo/sdk-api'; +import { TransactionType } from '@bitgo/sdk-core'; +import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; +import { fromBase64, toHex } from '@cosmjs/encoding'; +import should from 'should'; +import { Injective, Tinjective } from '../../../src'; +import * as testData from '../../resources/injective'; + +describe('Injective Transfer Builder', () => { + let bitgo: TestBitGoAPI; + let basecoin; + let factory; + let testTx; + let testTxWithMemo; + before(function () { + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); + bitgo.safeRegister('injective', Injective.createInstance); + bitgo.safeRegister('tinjective', Tinjective.createInstance); + bitgo.initializeTestVars(); + basecoin = bitgo.coin('tinjective'); + factory = basecoin.getBuilder(); + testTx = testData.TEST_SEND_TX; + testTxWithMemo = testData.TEST_TX_WITH_MEMO; + }); + + it('should build a Transfer tx with signature', async function () { + const txBuilder = factory.getTransferBuilder(); + txBuilder.sequence(testTx.sequence); + txBuilder.gasBudget(testTx.gasBudget); + txBuilder.messages([testTx.sendMessage.value]); + txBuilder.publicKey(toHex(fromBase64(testTx.pubKey))); + txBuilder.addSignature({ pub: toHex(fromBase64(testTx.pubKey)) }, Buffer.from(testTx.signature, 'base64')); + + const tx = await txBuilder.build(); + const json = await (await txBuilder.build()).toJson(); + should.equal(tx.type, TransactionType.Send); + should.deepEqual(json.gasBudget, testTx.gasBudget); + should.deepEqual(json.sendMessages, [testTx.sendMessage]); + should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey))); + should.deepEqual(json.sequence, testTx.sequence); + const rawTx = tx.toBroadcastFormat(); + should.equal(rawTx, testTx.signedTxBase64); + should.deepEqual(tx.inputs, [ + { + address: testData.TEST_SEND_TX.sender, + value: testData.TEST_SEND_TX.sendMessage.value.amount[0].amount, + coin: basecoin.getChain(), + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.TEST_SEND_TX.sendMessage.value.toAddress, + value: testData.TEST_SEND_TX.sendMessage.value.amount[0].amount, + coin: basecoin.getChain(), + }, + ]); + }); + + it('should build a Transfer tx with signature and memo', async function () { + const txBuilder = factory.getTransferBuilder(); + txBuilder.sequence(testTxWithMemo.sequence); + txBuilder.gasBudget(testTxWithMemo.gasBudget); + txBuilder.messages([testTxWithMemo.sendMessage.value]); + txBuilder.publicKey(toHex(fromBase64(testTxWithMemo.pubKey))); + txBuilder.memo(testTxWithMemo.memo); + txBuilder.addSignature( + { pub: toHex(fromBase64(testTxWithMemo.pubKey)) }, + Buffer.from(testTxWithMemo.signature, 'base64') + ); + + const tx = await txBuilder.build(); + const json = await (await txBuilder.build()).toJson(); + should.equal(tx.type, TransactionType.Send); + should.deepEqual(json.gasBudget, testTxWithMemo.gasBudget); + should.deepEqual(json.sendMessages, [testTxWithMemo.sendMessage]); + should.deepEqual(json.publicKey, toHex(fromBase64(testTxWithMemo.pubKey))); + should.deepEqual(json.sequence, testTxWithMemo.sequence); + should.equal(json.memo, testTxWithMemo.memo); + const rawTx = tx.toBroadcastFormat(); + should.equal(rawTx, testTxWithMemo.signedTxBase64); + should.deepEqual(tx.inputs, [ + { + address: testTxWithMemo.sendMessage.value.fromAddress, + value: testTxWithMemo.sendMessage.value.amount[0].amount, + coin: basecoin.getChain(), + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testTxWithMemo.sendMessage.value.toAddress, + value: testTxWithMemo.sendMessage.value.amount[0].amount, + coin: basecoin.getChain(), + }, + ]); + }); + + it('should build a Transfer tx without signature', async function () { + const txBuilder = factory.getTransferBuilder(); + txBuilder.sequence(testTx.sequence); + txBuilder.gasBudget(testTx.gasBudget); + txBuilder.messages([testTx.sendMessage.value]); + txBuilder.publicKey(toHex(fromBase64(testTx.pubKey))); + const tx = await txBuilder.build(); + const json = await (await txBuilder.build()).toJson(); + should.equal(tx.type, TransactionType.Send); + should.deepEqual(json.gasBudget, testTx.gasBudget); + should.deepEqual(json.sendMessages, [testTx.sendMessage]); + should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey))); + should.deepEqual(json.sequence, testTx.sequence); + tx.toBroadcastFormat(); + should.deepEqual(tx.inputs, [ + { + address: testData.TEST_SEND_TX.sender, + value: testData.TEST_SEND_TX.sendMessage.value.amount[0].amount, + coin: basecoin.getChain(), + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.TEST_SEND_TX.sendMessage.value.toAddress, + value: testData.TEST_SEND_TX.sendMessage.value.amount[0].amount, + coin: basecoin.getChain(), + }, + ]); + }); + + it('should sign a Transfer tx', async function () { + const txBuilder = factory.getTransferBuilder(); + txBuilder.sequence(testTx.sequence); + txBuilder.gasBudget(testTx.gasBudget); + txBuilder.messages([testTx.sendMessage.value]); + txBuilder.accountNumber(testTx.accountNumber); + txBuilder.chainId(testTx.chainId); + txBuilder.sign({ key: toHex(fromBase64(testTx.privateKey)) }); + const tx = await txBuilder.build(); + const json = await (await txBuilder.build()).toJson(); + should.equal(tx.type, TransactionType.Send); + should.deepEqual(json.gasBudget, testTx.gasBudget); + should.deepEqual(json.sendMessages, [testTx.sendMessage]); + should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey))); + should.deepEqual(json.sequence, testTx.sequence); + const rawTx = tx.toBroadcastFormat(); + should.equal(tx.signature[0], toHex(fromBase64(testTx.signature))); + should.equal(rawTx, testTx.signedTxBase64); + should.deepEqual(tx.inputs, [ + { + address: testData.TEST_SEND_TX.sender, + value: testData.TEST_SEND_TX.sendMessage.value.amount[0].amount, + coin: basecoin.getChain(), + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.TEST_SEND_TX.sendMessage.value.toAddress, + value: testData.TEST_SEND_TX.sendMessage.value.amount[0].amount, + coin: basecoin.getChain(), + }, + ]); + }); +}); diff --git a/modules/sdk-coin-injective/test/unit/utils.ts b/modules/sdk-coin-injective/test/unit/utils.ts new file mode 100644 index 0000000000..1ab21f36da --- /dev/null +++ b/modules/sdk-coin-injective/test/unit/utils.ts @@ -0,0 +1,47 @@ +import should from 'should'; + +import utils from '../../src/lib/utils'; +import * as testData from '../resources/injective'; +import { blockHash, txIds } from '../resources/injective'; + +describe('utils', () => { + it('should validate block hash correctly', () => { + should.equal(utils.isValidBlockId(blockHash.hash1), true); + should.equal(utils.isValidBlockId(blockHash.hash2), true); + // param is coming as undefined so it was causing an issue + should.equal(utils.isValidBlockId(undefined as unknown as string), false); + should.equal(utils.isValidBlockId(''), false); + }); + + it('should validate invalid block hash correctly', () => { + should.equal(utils.isValidBlockId(''), false); + should.equal(utils.isValidBlockId('0xade35465gfvdcsxsz24300'), false); + should.equal(utils.isValidBlockId(blockHash.hash2 + 'ff'), false); + should.equal(utils.isValidBlockId('latest'), false); + }); + + it('should validate transaction id correctly', () => { + should.equal(utils.isValidTransactionId(txIds.hash1), true); + should.equal(utils.isValidTransactionId(txIds.hash2), true); + should.equal(utils.isValidTransactionId(txIds.hash3), true); + }); + + it('should validate invalid transaction id correctly', () => { + should.equal(utils.isValidTransactionId(''), false); + should.equal(utils.isValidTransactionId(txIds.hash1.slice(3)), false); + should.equal(utils.isValidTransactionId(txIds.hash3 + '00'), false); + should.equal(utils.isValidTransactionId('dalij43ta0ga2dadda02'), false); + }); + + it('validateAmount', function () { + should.doesNotThrow(() => utils.validateAmountData([testData.coinAmounts.amount1])); + should.doesNotThrow(() => utils.validateAmountData([testData.coinAmounts.amount2])); + should.doesNotThrow(() => utils.validateAmountData([testData.coinAmounts.amount3])); + should(() => utils.validateAmountData([testData.coinAmounts.amount4])).throwError( + 'transactionBuilder: validateAmount: Invalid amount: ' + testData.coinAmounts.amount4.amount + ); + should(() => utils.validateAmountData([testData.coinAmounts.amount5])).throwError( + 'transactionBuilder: validateAmount: Invalid denom: ' + testData.coinAmounts.amount5.denom + ); + }); +}); diff --git a/modules/sdk-coin-injective/tsconfig.json b/modules/sdk-coin-injective/tsconfig.json new file mode 100644 index 0000000000..d40e35bb27 --- /dev/null +++ b/modules/sdk-coin-injective/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./", + "strictPropertyInitialization": false, + "esModuleInterop": true, + "typeRoots": ["../../types", "./node_modules/@types", "../../node_modules/@types"] + }, + "include": ["src/**/*", "test/**/*"], + "exclude": ["node_modules"], + "references": [ + { + "path": "../sdk-api" + }, + { + "path": "../sdk-core" + }, + { + "path": "../sdk-test" + }, + { + "path": "../abstract-cosmos" + } + ] +} diff --git a/modules/sdk-core/src/bitgo/environments.ts b/modules/sdk-core/src/bitgo/environments.ts index 493b022a74..baf663f5e9 100644 --- a/modules/sdk-core/src/bitgo/environments.ts +++ b/modules/sdk-core/src/bitgo/environments.ts @@ -24,6 +24,7 @@ interface EnvironmentTemplate { solNodeUrl: string; adaNodeUrl: string; hashNodeUrl: string; + injNodeUrl: string; atomNodeUrl: string; osmoNodeUrl: string; tiaNodeUrl: string; @@ -102,6 +103,7 @@ const mainnetBase: EnvironmentTemplate = { solNodeUrl: 'https://api.mainnet-beta.solana.com', adaNodeUrl: 'https://api.koios.rest/api/v0', hashNodeUrl: 'https://api.provenance.io', + injNodeUrl: 'https://k8s.global.mainnet.lcd.injective.network', atomNodeUrl: 'https://rest.cosmos.directory/cosmoshub/', osmoNodeUrl: 'https://lcd.osmosis.zone', tiaNodeUrl: 'https://api-mocha.pops.one', // TODO(BG-78997): Celestia is still only in testnet update to mainnet url when it's live @@ -137,6 +139,7 @@ const testnetBase: EnvironmentTemplate = { solNodeUrl: 'https://api.devnet.solana.com', adaNodeUrl: 'https://preprod.koios.rest/api/v0', hashNodeUrl: 'https://api.test.provenance.io', + injNodeUrl: 'https://k8s.testnet.lcd.injective.network', atomNodeUrl: 'https://rest.sentry-02.theta-testnet.polypore.xyz/', osmoNodeUrl: 'https://lcd.osmotest5.osmosis.zone', tiaNodeUrl: 'https://api-mocha.pops.one', diff --git a/modules/statics/src/base.ts b/modules/statics/src/base.ts index 96b9bfa148..7230b16672 100644 --- a/modules/statics/src/base.ts +++ b/modules/statics/src/base.ts @@ -45,7 +45,7 @@ export enum CoinFamily { FIAT = 'fiat', HASH = 'hash', // Provenance HBAR = 'hbar', - INJ = 'injective', + INJECTIVE = 'injective', LTC = 'ltc', POLYGON = 'polygon', NEAR = 'near', @@ -237,6 +237,7 @@ export enum UnderlyingAsset { GTC = 'gtc', HASH = 'hash', // Provenance HBAR = 'hbar', // Hedera main coin + INJECTIVE = 'injective', LTC = 'ltc', NEAR = 'near', OSMO = 'osmo', @@ -1276,7 +1277,7 @@ export enum BaseUnit { HASH = 'nhash', BLD = 'ubld', SEI = 'usei', - INJ = 'inj', + INJECTIVE = 'inj', } export interface BaseCoinConstructorOptions { diff --git a/modules/statics/src/coins.ts b/modules/statics/src/coins.ts index e994603deb..e2b64d1444 100644 --- a/modules/statics/src/coins.ts +++ b/modules/statics/src/coins.ts @@ -189,7 +189,7 @@ const TIA_FEATURES = [...AccountCoin.DEFAULT_FEATURES, CoinFeature.TSS, CoinFeat const HASH_FEATURES = [...AccountCoin.DEFAULT_FEATURES, CoinFeature.TSS, CoinFeature.STAKING]; const BLD_FEATURES = [...AccountCoin.DEFAULT_FEATURES, CoinFeature.TSS, CoinFeature.STAKING]; const SEI_FEATURES = [...AccountCoin.DEFAULT_FEATURES, CoinFeature.TSS, CoinFeature.STAKING]; -const INJ_FEATURES = [...AccountCoin.DEFAULT_FEATURES, CoinFeature.TSS, CoinFeature.STAKING]; +const INJECTIVE_FEATURES = [...AccountCoin.DEFAULT_FEATURES, CoinFeature.TSS, CoinFeature.STAKING]; const GENERIC_TOKEN_FEATURES = [ CoinFeature.ACCOUNT_MODEL, @@ -868,21 +868,21 @@ export const coins = CoinMap.fromCoins([ '5f9506c5-f10a-43c2-92d3-52941083bbc7', 'injective', 'Injective', - Networks.main.inj, + Networks.main.injective, 18, - UnderlyingAsset.INJ, - BaseUnit.INJ, - INJ_FEATURES + UnderlyingAsset.INJECTIVE, + BaseUnit.INJECTIVE, + INJECTIVE_FEATURES ), account( '6ae81d6a-011c-499c-a3c8-15ac7dcac48a', 'tinjective', 'Testnet Injective', - Networks.test.inj, + Networks.test.injective, 18, - UnderlyingAsset.INJ, - BaseUnit.INJ, - INJ_FEATURES + UnderlyingAsset.INJECTIVE, + BaseUnit.INJECTIVE, + INJECTIVE_FEATURES ), account( 'e48baabf-5cc9-4011-b67e-6f6425753df2', diff --git a/modules/statics/src/networks.ts b/modules/statics/src/networks.ts index 78dbb36719..4f8f08d6bc 100644 --- a/modules/statics/src/networks.ts +++ b/modules/statics/src/networks.ts @@ -631,13 +631,13 @@ class SeiTestnet extends Testnet implements AccountNetwork { class Injective extends Mainnet implements AccountNetwork { name = 'Injective'; - family = CoinFamily.INJ; + family = CoinFamily.INJECTIVE; explorerUrl = 'https://explorer.injective.network/transaction/'; } class InjectiveTestnet extends Testnet implements AccountNetwork { name = 'InjectiveTestnet'; - family = CoinFamily.INJ; + family = CoinFamily.INJECTIVE; explorerUrl = 'https://testnet.explorer.injective.network/transaction/'; } @@ -804,7 +804,7 @@ export const Networks = { fiat: Object.freeze(new Fiat()), hash: Object.freeze(new Hash()), hedera: Object.freeze(new Hedera()), - inj: Object.freeze(new Injective()), + injective: Object.freeze(new Injective()), litecoin: Object.freeze(new Litecoin()), polygon: Object.freeze(new Polygon()), ofc: Object.freeze(new Ofc()), @@ -847,7 +847,7 @@ export const Networks = { ethereumClassicTestnet: Object.freeze(new EthereumClassicTestnet()), hash: Object.freeze(new HashTestnet()), hedera: Object.freeze(new HederaTestnet()), - inj: Object.freeze(new InjectiveTestnet()), + injective: Object.freeze(new InjectiveTestnet()), kovan: Object.freeze(new Kovan()), goerli: Object.freeze(new Goerli()), litecoin: Object.freeze(new LitecoinTestnet()), diff --git a/tsconfig.packages.json b/tsconfig.packages.json index c4f20b3d46..86a71062b8 100644 --- a/tsconfig.packages.json +++ b/tsconfig.packages.json @@ -100,6 +100,9 @@ { "path": "./modules/sdk-coin-hbar" }, + { + "path": "./modules/sdk-coin-injective" + }, { "path": "./modules/sdk-coin-ltc" },