From 86ab88599761fe8f53bc88bd630651336000b38c Mon Sep 17 00:00:00 2001 From: Hitansh Madan Date: Mon, 9 Oct 2023 13:29:27 +0530 Subject: [PATCH] feat(sdk-coin-islm): add Islamic Coin Ticket: WIN-490 --- Dockerfile | 3 + modules/abstract-cosmos/src/cosmosCoin.ts | 15 +- modules/abstract-cosmos/src/lib/iface.ts | 10 + .../src/lib/transactionBuilder.ts | 5 +- modules/abstract-cosmos/src/lib/utils.ts | 19 +- modules/account-lib/package.json | 1 + modules/account-lib/src/index.ts | 5 + modules/account-lib/tsconfig.json | 3 + modules/bitgo/package.json | 1 + modules/bitgo/src/v2/coinFactory.ts | 4 + modules/bitgo/src/v2/coins/index.ts | 2 + modules/bitgo/tsconfig.json | 3 + modules/sdk-coin-islm/.eslintignore | 5 + modules/sdk-coin-islm/.gitignore | 3 + modules/sdk-coin-islm/.mocharc.yml | 8 + modules/sdk-coin-islm/.npmignore | 14 + modules/sdk-coin-islm/.prettierignore | 2 + modules/sdk-coin-islm/.prettierrc.yml | 3 + modules/sdk-coin-islm/README.md | 30 ++ modules/sdk-coin-islm/package.json | 60 +++ modules/sdk-coin-islm/resources/README.md | 31 ++ .../crypto/v1/ethsecp256k1/ethSecp256k1.proto | 23 ++ .../resources/proto/gogoproto/gogo.proto | 145 +++++++ .../proto/google/api/annotations.proto | 31 ++ .../resources/proto/google/api/http.proto | 375 ++++++++++++++++++ .../resources/types/ethSecp256k1.ts | 183 +++++++++ modules/sdk-coin-islm/src/index.ts | 4 + modules/sdk-coin-islm/src/islm.ts | 72 ++++ modules/sdk-coin-islm/src/lib/constants.ts | 6 + modules/sdk-coin-islm/src/lib/index.ts | 10 + modules/sdk-coin-islm/src/lib/keyPair.ts | 19 + .../src/lib/transactionBuilderFactory.ts | 78 ++++ modules/sdk-coin-islm/src/lib/utils.ts | 86 ++++ modules/sdk-coin-islm/src/register.ts | 8 + modules/sdk-coin-islm/src/tislm.ts | 25 ++ modules/sdk-coin-islm/test/resources/islm.ts | 233 +++++++++++ modules/sdk-coin-islm/test/unit/islm.ts | 339 ++++++++++++++++ modules/sdk-coin-islm/test/unit/keyPair.ts | 117 ++++++ .../sdk-coin-islm/test/unit/transaction.ts | 232 +++++++++++ .../StakingActivateBuilder.ts | 121 ++++++ .../StakingDeactivateBuilder.ts | 119 ++++++ .../StakingWithdrawRewardsBuilder.ts | 121 ++++++ .../transactionBuilder/transactionBuilder.ts | 83 ++++ .../transactionBuilder/transferBuilder.ts | 160 ++++++++ modules/sdk-coin-islm/test/unit/utils.ts | 47 +++ modules/sdk-coin-islm/tsconfig.json | 26 ++ modules/sdk-core/src/bitgo/environments.ts | 3 + tsconfig.packages.json | 3 + 48 files changed, 2882 insertions(+), 14 deletions(-) create mode 100644 modules/sdk-coin-islm/.eslintignore create mode 100644 modules/sdk-coin-islm/.gitignore create mode 100644 modules/sdk-coin-islm/.mocharc.yml create mode 100644 modules/sdk-coin-islm/.npmignore create mode 100644 modules/sdk-coin-islm/.prettierignore create mode 100644 modules/sdk-coin-islm/.prettierrc.yml create mode 100644 modules/sdk-coin-islm/README.md create mode 100644 modules/sdk-coin-islm/package.json create mode 100644 modules/sdk-coin-islm/resources/README.md create mode 100644 modules/sdk-coin-islm/resources/proto/ethermint/crypto/v1/ethsecp256k1/ethSecp256k1.proto create mode 100644 modules/sdk-coin-islm/resources/proto/gogoproto/gogo.proto create mode 100644 modules/sdk-coin-islm/resources/proto/google/api/annotations.proto create mode 100644 modules/sdk-coin-islm/resources/proto/google/api/http.proto create mode 100644 modules/sdk-coin-islm/resources/types/ethSecp256k1.ts create mode 100644 modules/sdk-coin-islm/src/index.ts create mode 100644 modules/sdk-coin-islm/src/islm.ts create mode 100644 modules/sdk-coin-islm/src/lib/constants.ts create mode 100644 modules/sdk-coin-islm/src/lib/index.ts create mode 100644 modules/sdk-coin-islm/src/lib/keyPair.ts create mode 100644 modules/sdk-coin-islm/src/lib/transactionBuilderFactory.ts create mode 100644 modules/sdk-coin-islm/src/lib/utils.ts create mode 100644 modules/sdk-coin-islm/src/register.ts create mode 100644 modules/sdk-coin-islm/src/tislm.ts create mode 100644 modules/sdk-coin-islm/test/resources/islm.ts create mode 100644 modules/sdk-coin-islm/test/unit/islm.ts create mode 100644 modules/sdk-coin-islm/test/unit/keyPair.ts create mode 100644 modules/sdk-coin-islm/test/unit/transaction.ts create mode 100644 modules/sdk-coin-islm/test/unit/transactionBuilder/StakingActivateBuilder.ts create mode 100644 modules/sdk-coin-islm/test/unit/transactionBuilder/StakingDeactivateBuilder.ts create mode 100644 modules/sdk-coin-islm/test/unit/transactionBuilder/StakingWithdrawRewardsBuilder.ts create mode 100644 modules/sdk-coin-islm/test/unit/transactionBuilder/transactionBuilder.ts create mode 100644 modules/sdk-coin-islm/test/unit/transactionBuilder/transferBuilder.ts create mode 100644 modules/sdk-coin-islm/test/unit/utils.ts create mode 100644 modules/sdk-coin-islm/tsconfig.json diff --git a/Dockerfile b/Dockerfile index ad7a8ccb86..413cf29c9f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -69,6 +69,7 @@ COPY --from=builder /tmp/bitgo/modules/sdk-coin-eth2 /var/modules/sdk-coin-eth2/ COPY --from=builder /tmp/bitgo/modules/sdk-coin-hash /var/modules/sdk-coin-hash/ COPY --from=builder /tmp/bitgo/modules/sdk-coin-hbar /var/modules/sdk-coin-hbar/ COPY --from=builder /tmp/bitgo/modules/sdk-coin-injective /var/modules/sdk-coin-injective/ +COPY --from=builder /tmp/bitgo/modules/sdk-coin-islm /var/modules/sdk-coin-islm/ COPY --from=builder /tmp/bitgo/modules/sdk-coin-near /var/modules/sdk-coin-near/ COPY --from=builder /tmp/bitgo/modules/sdk-coin-osmo /var/modules/sdk-coin-osmo/ COPY --from=builder /tmp/bitgo/modules/sdk-coin-polygon /var/modules/sdk-coin-polygon/ @@ -129,6 +130,7 @@ cd /var/modules/sdk-coin-eth2 && yarn link && \ cd /var/modules/sdk-coin-hash && yarn link && \ cd /var/modules/sdk-coin-hbar && yarn link && \ cd /var/modules/sdk-coin-injective && yarn link && \ +cd /var/modules/sdk-coin-islm && yarn link && \ cd /var/modules/sdk-coin-near && yarn link && \ cd /var/modules/sdk-coin-osmo && yarn link && \ cd /var/modules/sdk-coin-polygon && yarn link && \ @@ -192,6 +194,7 @@ RUN cd /var/bitgo-express && \ yarn link @bitgo/sdk-coin-hash && \ yarn link @bitgo/sdk-coin-hbar && \ yarn link @bitgo/sdk-coin-injective && \ + yarn link @bitgo/sdk-coin-islm && \ yarn link @bitgo/sdk-coin-near && \ yarn link @bitgo/sdk-coin-osmo && \ yarn link @bitgo/sdk-coin-polygon && \ diff --git a/modules/abstract-cosmos/src/cosmosCoin.ts b/modules/abstract-cosmos/src/cosmosCoin.ts index 22051bb295..a269e07857 100644 --- a/modules/abstract-cosmos/src/cosmosCoin.ts +++ b/modules/abstract-cosmos/src/cosmosCoin.ts @@ -27,7 +27,7 @@ import { bip32 } from '@bitgo/utxo-lib'; import { Coin } from '@cosmjs/stargate'; import { BigNumber } from 'bignumber.js'; import { Buffer } from 'buffer'; -import { createHash, Hash, randomBytes } from 'crypto'; +import { Hash, randomBytes } from 'crypto'; import * as _ from 'lodash'; import * as querystring from 'querystring'; import * as request from 'superagent'; @@ -43,7 +43,6 @@ import { } from './lib'; import { ROOT_PATH } from './lib/constants'; import utils from './lib/utils'; - /** * Cosmos accounts support memo Id based addresses */ @@ -227,7 +226,7 @@ export class CosmosCoin extends BaseCoin { // Step 7: Sign the tx const signature = await this.signRecoveryTSS(userKeyCombined, backupKeyCombined, signableHex); const signableBuffer = Buffer.from(signableHex, 'hex'); - MPC.verify(signableBuffer, signature, createHash('sha256')); + MPC.verify(signableBuffer, signature, this.getHashFunction()); const cosmosKeyPair = this.getKeyPair(publicKey); txnBuilder.addSignature({ pub: cosmosKeyPair.getKeys().pub }, Buffer.from(signature.r + signature.s, 'hex')); const signedTransaction = await txnBuilder.build(); @@ -382,8 +381,8 @@ export class CosmosCoin extends BaseCoin { const MESSAGE = Buffer.from(txHex, 'hex'); const [signA, signB] = [ - MPC.sign(MESSAGE, signCombineOne.oShare, signCombineTwo.dShare, createHash('sha256')), - MPC.sign(MESSAGE, signCombineTwo.oShare, signCombineOne.dShare, createHash('sha256')), + MPC.sign(MESSAGE, signCombineOne.oShare, signCombineTwo.dShare, this.getHashFunction()), + MPC.sign(MESSAGE, signCombineTwo.oShare, signCombineOne.dShare, this.getHashFunction()), ]; return MPC.constructSignature([signA, signB]); @@ -620,11 +619,11 @@ export class CosmosCoin extends BaseCoin { } /** - * Retrieves the SHA256 hash function. - * @returns {Hash} The SHA256 hash function. + * Retrieves the hash function. + * @returns {Hash} The hash function. */ getHashFunction(): Hash { - return createHash('sha256'); + return utils.getHashFunction(); } /** diff --git a/modules/abstract-cosmos/src/lib/iface.ts b/modules/abstract-cosmos/src/lib/iface.ts index a1972e8155..a175082c81 100644 --- a/modules/abstract-cosmos/src/lib/iface.ts +++ b/modules/abstract-cosmos/src/lib/iface.ts @@ -1,6 +1,16 @@ import { TransactionExplanation as BaseTransactionExplanation, TransactionType } from '@bitgo/sdk-core'; import { Coin } from '@cosmjs/stargate'; +export enum PubKeyTypeUrl { + secp256k1 = '/cosmos.crypto.secp256k1.PubKey', + ethSecp256k1 = '/ethermint.crypto.v1.ethsecp256k1.PubKey', +} + +export enum PubKeyType { + secp256k1 = 'tendermint/PubKeySecp256k1', + ethSecp256k1 = 'tendermint/PubKeyEthSecp256k1', +} + export interface TransactionExplanation extends BaseTransactionExplanation { type: TransactionType; } diff --git a/modules/abstract-cosmos/src/lib/transactionBuilder.ts b/modules/abstract-cosmos/src/lib/transactionBuilder.ts index 65509138a9..3a350aca18 100644 --- a/modules/abstract-cosmos/src/lib/transactionBuilder.ts +++ b/modules/abstract-cosmos/src/lib/transactionBuilder.ts @@ -9,13 +9,14 @@ import { TransactionType, } from '@bitgo/sdk-core'; import { BaseCoin as CoinConfig } from '@bitgo/statics'; -import { Secp256k1, sha256 } from '@cosmjs/crypto'; +import { Secp256k1 } from '@cosmjs/crypto'; import { makeSignBytes } from '@cosmjs/proto-signing'; import BigNumber from 'bignumber.js'; import { CosmosTransactionMessage, FeeData, MessageData } from './iface'; import { CosmosKeyPair as KeyPair } from './keyPair'; import { CosmosTransaction } from './transaction'; import { CosmosUtils } from './utils'; +import { fromHex } from '@cosmjs/encoding'; export abstract class CosmosTransactionBuilder extends BaseTransactionBuilder { protected _transaction: CosmosTransaction; @@ -198,7 +199,7 @@ export abstract class CosmosTransactionBuilder extends BaseTransactionBuilder { this._accountNumber, this._chainId ); - const txnHash = sha256(makeSignBytes(signDoc)); + const txnHash = fromHex(this._utils.getHashFunction().update(makeSignBytes(signDoc)).digest().toString('hex')); const signature = await Secp256k1.createSignature(txnHash, privateKey); const compressedSig = Buffer.concat([signature.r(), signature.s()]); this.addSignature({ pub: this.transaction.cosmosLikeTransaction.publicKey }, compressedSig); diff --git a/modules/abstract-cosmos/src/lib/utils.ts b/modules/abstract-cosmos/src/lib/utils.ts index d461cfa3b1..3cf19b30a5 100644 --- a/modules/abstract-cosmos/src/lib/utils.ts +++ b/modules/abstract-cosmos/src/lib/utils.ts @@ -23,7 +23,7 @@ import { SignDoc, TxRaw } from 'cosmjs-types/cosmos/tx/v1beta1/tx'; import { Any } from 'cosmjs-types/google/protobuf/any'; import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx'; -import * as crypto from 'crypto'; +import { Hash, createHash } from 'crypto'; import * as constants from './constants'; import { CosmosLikeTransaction, @@ -309,6 +309,10 @@ export class CosmosUtils implements BaseUtils { } } + getEncodedPubkey(pubkey: string): Any { + return encodePubkey(encodeSecp256k1Pubkey(fromHex(pubkey))); + } + /** * Creates a txRaw from an cosmos like transaction @see CosmosLikeTransaction * @Precondition cosmosLikeTransaction.publicKey must be defined @@ -319,7 +323,7 @@ export class CosmosUtils implements BaseUtils { if (!cosmosLikeTransaction.publicKey) { throw new Error('publicKey is required to create a txRaw'); } - const encodedPublicKey: Any = encodePubkey(encodeSecp256k1Pubkey(fromHex(cosmosLikeTransaction.publicKey))); + const encodedPublicKey: Any = this.getEncodedPubkey(cosmosLikeTransaction.publicKey); const messages = cosmosLikeTransaction.sendMessages as unknown as Any[]; let txBodyValue; if (cosmosLikeTransaction.memo) { @@ -597,8 +601,7 @@ export class CosmosUtils implements BaseUtils { authInfoBytes: unsignedTx.authInfoBytes, signatures: [signature], }); - hash = crypto - .createHash('sha256') + hash = createHash('sha256') .update(TxRaw.encode(signedTx).finish()) .digest() .toString('hex') @@ -738,6 +741,14 @@ export class CosmosUtils implements BaseUtils { this.validateAmountData(message.funds, transactionType); } } + + /** + * Retrieves the hash function. + * @returns {Hash} The hash function. + */ + getHashFunction(): Hash { + return createHash('sha256'); + } } const utils = new CosmosUtils(); diff --git a/modules/account-lib/package.json b/modules/account-lib/package.json index f1af5b193c..1cc236d536 100644 --- a/modules/account-lib/package.json +++ b/modules/account-lib/package.json @@ -42,6 +42,7 @@ "@bitgo/sdk-coin-hash": "^1.4.11", "@bitgo/sdk-coin-hbar": "^1.5.11", "@bitgo/sdk-coin-injective": "^1.4.11", + "@bitgo/sdk-coin-islm": "^1.0.0", "@bitgo/sdk-coin-near": "^1.6.11", "@bitgo/sdk-coin-osmo": "^1.6.11", "@bitgo/sdk-coin-polygon": "^1.8.0", diff --git a/modules/account-lib/src/index.ts b/modules/account-lib/src/index.ts index 170727dbc0..e34a8fdd73 100644 --- a/modules/account-lib/src/index.ts +++ b/modules/account-lib/src/index.ts @@ -86,6 +86,9 @@ export { Sei }; import * as Injective from '@bitgo/sdk-coin-injective'; export { Injective }; +import * as Islm from '@bitgo/sdk-coin-islm'; +export { Islm }; + import * as Zeta from '@bitgo/sdk-coin-zeta'; export { Zeta }; @@ -160,6 +163,8 @@ const coinBuilderMap = { tinjective: Injective.TransactionBuilderFactory, zeta: Zeta.TransactionBuilderFactory, tzeta: Zeta.TransactionBuilderFactory, + islm: Islm.TransactionBuilderFactory, + tislm: Islm.TransactionBuilderFactory, }; /** diff --git a/modules/account-lib/tsconfig.json b/modules/account-lib/tsconfig.json index d2bf1dba20..bf53d64bb6 100644 --- a/modules/account-lib/tsconfig.json +++ b/modules/account-lib/tsconfig.json @@ -52,6 +52,9 @@ { "path": "../sdk-coin-injective" }, + { + "path": "../sdk-coin-islm" + }, { "path": "../sdk-coin-near" }, diff --git a/modules/bitgo/package.json b/modules/bitgo/package.json index 4e17b7c64b..2a72d1a3d8 100644 --- a/modules/bitgo/package.json +++ b/modules/bitgo/package.json @@ -73,6 +73,7 @@ "@bitgo/sdk-coin-hash": "^1.4.11", "@bitgo/sdk-coin-hbar": "^1.5.11", "@bitgo/sdk-coin-injective": "^1.4.11", + "@bitgo/sdk-coin-islm": "^1.0.0", "@bitgo/sdk-coin-ltc": "^2.2.11", "@bitgo/sdk-coin-near": "^1.6.11", "@bitgo/sdk-coin-osmo": "^1.6.11", diff --git a/modules/bitgo/src/v2/coinFactory.ts b/modules/bitgo/src/v2/coinFactory.ts index 4023350788..c51fb4de70 100644 --- a/modules/bitgo/src/v2/coinFactory.ts +++ b/modules/bitgo/src/v2/coinFactory.ts @@ -44,6 +44,7 @@ import { Hash, Hbar, Injective, + Islm, Ltc, Ofc, OfcToken, @@ -84,6 +85,7 @@ import { Thbar, Tia, Tinjective, + Tislm, Tltc, Tosmo, Tpolygon, @@ -140,6 +142,7 @@ function registerCoinConstructors(globalCoinFactory: CoinFactory): void { globalCoinFactory.register('hbar', Hbar.createInstance); globalCoinFactory.register('ltc', Ltc.createInstance); globalCoinFactory.register('injective', Injective.createInstance); + globalCoinFactory.register('islm', Islm.createInstance); globalCoinFactory.register('near', Near.createInstance); globalCoinFactory.register('ofc', Ofc.createInstance); globalCoinFactory.register('osmo', Osmo.createInstance); @@ -178,6 +181,7 @@ function registerCoinConstructors(globalCoinFactory: CoinFactory): void { globalCoinFactory.register('thash', Thash.createInstance); globalCoinFactory.register('thbar', Thbar.createInstance); globalCoinFactory.register('tinjective', Tinjective.createInstance); + globalCoinFactory.register('tislm', Tislm.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 ce4d89b994..284cadbee7 100644 --- a/modules/bitgo/src/v2/coins/index.ts +++ b/modules/bitgo/src/v2/coins/index.ts @@ -24,6 +24,7 @@ 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 { Islm, Tislm } from '@bitgo/sdk-coin-islm'; 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'; @@ -76,6 +77,7 @@ export { Ton, Tton }; export { Bld, Tbld }; export { Sei, Tsei }; export { Injective, Tinjective }; +export { Islm, Tislm }; export { Trx, Ttrx }; export { StellarToken, Txlm, Xlm }; export { Txrp, Xrp }; diff --git a/modules/bitgo/tsconfig.json b/modules/bitgo/tsconfig.json index 39bdfc579d..a4d4cf7423 100644 --- a/modules/bitgo/tsconfig.json +++ b/modules/bitgo/tsconfig.json @@ -110,6 +110,9 @@ { "path": "../sdk-coin-injective" }, + { + "path": "../sdk-coin-islm" + }, { "path": "../sdk-coin-ltc" }, diff --git a/modules/sdk-coin-islm/.eslintignore b/modules/sdk-coin-islm/.eslintignore new file mode 100644 index 0000000000..190f83e0df --- /dev/null +++ b/modules/sdk-coin-islm/.eslintignore @@ -0,0 +1,5 @@ +node_modules +.idea +public +dist + diff --git a/modules/sdk-coin-islm/.gitignore b/modules/sdk-coin-islm/.gitignore new file mode 100644 index 0000000000..67ccce4c64 --- /dev/null +++ b/modules/sdk-coin-islm/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.idea/ +dist/ diff --git a/modules/sdk-coin-islm/.mocharc.yml b/modules/sdk-coin-islm/.mocharc.yml new file mode 100644 index 0000000000..95814796d1 --- /dev/null +++ b/modules/sdk-coin-islm/.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-islm/.npmignore b/modules/sdk-coin-islm/.npmignore new file mode 100644 index 0000000000..d5fb3a098c --- /dev/null +++ b/modules/sdk-coin-islm/.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-islm/.prettierignore b/modules/sdk-coin-islm/.prettierignore new file mode 100644 index 0000000000..3a11d6af29 --- /dev/null +++ b/modules/sdk-coin-islm/.prettierignore @@ -0,0 +1,2 @@ +.nyc_output/ +dist/ diff --git a/modules/sdk-coin-islm/.prettierrc.yml b/modules/sdk-coin-islm/.prettierrc.yml new file mode 100644 index 0000000000..7c3d8dd32a --- /dev/null +++ b/modules/sdk-coin-islm/.prettierrc.yml @@ -0,0 +1,3 @@ +printWidth: 120 +singleQuote: true +trailingComma: 'es5' diff --git a/modules/sdk-coin-islm/README.md b/modules/sdk-coin-islm/README.md new file mode 100644 index 0000000000..45d88d7605 --- /dev/null +++ b/modules/sdk-coin-islm/README.md @@ -0,0 +1,30 @@ +# BitGo sdk-coin-islm + +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-islm`. + +```shell +npm i @bitgo/sdk-api @bitgo/sdk-coin-islm +``` + +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 { Islm } from '@bitgo/sdk-coin-islm'; + +const sdk = new BitGoAPI(); + +sdk.register('islm', Islm.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-islm/package.json b/modules/sdk-coin-islm/package.json new file mode 100644 index 0000000000..021bb96af7 --- /dev/null +++ b/modules/sdk-coin-islm/package.json @@ -0,0 +1,60 @@ +{ + "name": "@bitgo/sdk-coin-islm", + "version": "1.0.0", + "description": "BitGo SDK coin library for Islm", + "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": ">=16 <19" + }, + "repository": { + "type": "git", + "url": "https://github.com/BitGo/BitGoJS.git", + "directory": "modules/sdk-coin-islm" + }, + "lint-staged": { + "*.{js,ts}": [ + "yarn prettier --write", + "yarn eslint --fix" + ] + }, + "publishConfig": { + "access": "public" + }, + "nyc": { + "extension": [ + ".ts" + ] + }, + "dependencies": { + "@bitgo/abstract-cosmos": "^1.6.11", + "@bitgo/sdk-core": "^8.24.0", + "@bitgo/statics": "^27.0.0", + "@cosmjs/amino": "^0.29.5", + "@cosmjs/encoding": "^0.29.5", + "@cosmjs/proto-signing": "^0.29.5", + "@cosmjs/stargate": "^0.29.5", + "bignumber.js": "^9.1.1", + "cosmjs-types": "^0.6.1", + "ethers": "^5.7.2", + "keccak": "3.0.3", + "protobufjs": "^7.2.4" + }, + "devDependencies": { + "@bitgo/sdk-api": "^1.22.0", + "@bitgo/sdk-test": "^1.2.41" + } +} diff --git a/modules/sdk-coin-islm/resources/README.md b/modules/sdk-coin-islm/resources/README.md new file mode 100644 index 0000000000..0123347f19 --- /dev/null +++ b/modules/sdk-coin-islm/resources/README.md @@ -0,0 +1,31 @@ +# Resources + +This directory contains external logic required to handle signing/validation/address derivation for +a specific coin. + +Typically, this manifests as a smaller snippet of code from a larger codebase, of which, this +library only requires a small portion of. + +--- + +## ISLM Resources + +Haqq Network (Islamic Coin) uses [protocol buffers (protobufs)](https://developers.google.com/protocol-buffers). Instead of +pulling a bunch of non-relevant Tron full node code into this library as a dependency, we grab +exactly what we need and add it in `resources/`. + +### Generating protobufs for Tron + +Static code for Tron protobufs is already included. Generating at build time can be problematic, until +[this issue](https://github.com/protobufjs/protobuf.js/issues/1477) is resolved. Should we ever need to +update the static code, the following commands can be run: + +```bash +npm run gen-protobuf +``` + +To generate the respective TypeScript definitions: + +```bash +npm run gen-protobufts +``` diff --git a/modules/sdk-coin-islm/resources/proto/ethermint/crypto/v1/ethsecp256k1/ethSecp256k1.proto b/modules/sdk-coin-islm/resources/proto/ethermint/crypto/v1/ethsecp256k1/ethSecp256k1.proto new file mode 100644 index 0000000000..7f4e46469e --- /dev/null +++ b/modules/sdk-coin-islm/resources/proto/ethermint/crypto/v1/ethsecp256k1/ethSecp256k1.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; +package ethermint.crypto.v1.ethsecp256k1; + +import "gogoproto/gogo.proto"; + +option go_package = "github.com/evmos/ethermint/crypto/ethsecp256k1"; + +// PubKey defines a type alias for an ecdsa.PublicKey that implements +// Tendermint's PubKey interface. It represents the 33-byte compressed public +// key format. +message PubKey { + option (gogoproto.goproto_stringer) = false; + + // key is the public key in byte form + bytes key = 1; +} + +// PrivKey defines a type alias for an ecdsa.PrivateKey that implements +// Tendermint's PrivateKey interface. +message PrivKey { + // key is the private key in byte form + bytes key = 1; +} diff --git a/modules/sdk-coin-islm/resources/proto/gogoproto/gogo.proto b/modules/sdk-coin-islm/resources/proto/gogoproto/gogo.proto new file mode 100644 index 0000000000..974b36a7cc --- /dev/null +++ b/modules/sdk-coin-islm/resources/proto/gogoproto/gogo.proto @@ -0,0 +1,145 @@ +// Protocol Buffers for Go with Gadgets +// +// Copyright (c) 2013, The GoGo Authors. All rights reserved. +// http://github.com/cosmos/gogoproto +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto2"; +package gogoproto; + +import "google/protobuf/descriptor.proto"; + +option java_package = "com.google.protobuf"; +option java_outer_classname = "GoGoProtos"; +option go_package = "github.com/cosmos/gogoproto/gogoproto"; + +extend google.protobuf.EnumOptions { + optional bool goproto_enum_prefix = 62001; + optional bool goproto_enum_stringer = 62021; + optional bool enum_stringer = 62022; + optional string enum_customname = 62023; + optional bool enumdecl = 62024; +} + +extend google.protobuf.EnumValueOptions { + optional string enumvalue_customname = 66001; +} + +extend google.protobuf.FileOptions { + optional bool goproto_getters_all = 63001; + optional bool goproto_enum_prefix_all = 63002; + optional bool goproto_stringer_all = 63003; + optional bool verbose_equal_all = 63004; + optional bool face_all = 63005; + optional bool gostring_all = 63006; + optional bool populate_all = 63007; + optional bool stringer_all = 63008; + optional bool onlyone_all = 63009; + + optional bool equal_all = 63013; + optional bool description_all = 63014; + optional bool testgen_all = 63015; + optional bool benchgen_all = 63016; + optional bool marshaler_all = 63017; + optional bool unmarshaler_all = 63018; + optional bool stable_marshaler_all = 63019; + + optional bool sizer_all = 63020; + + optional bool goproto_enum_stringer_all = 63021; + optional bool enum_stringer_all = 63022; + + optional bool unsafe_marshaler_all = 63023; + optional bool unsafe_unmarshaler_all = 63024; + + optional bool goproto_extensions_map_all = 63025; + optional bool goproto_unrecognized_all = 63026; + optional bool gogoproto_import = 63027; + optional bool protosizer_all = 63028; + optional bool compare_all = 63029; + optional bool typedecl_all = 63030; + optional bool enumdecl_all = 63031; + + optional bool goproto_registration = 63032; + optional bool messagename_all = 63033; + + optional bool goproto_sizecache_all = 63034; + optional bool goproto_unkeyed_all = 63035; +} + +extend google.protobuf.MessageOptions { + optional bool goproto_getters = 64001; + optional bool goproto_stringer = 64003; + optional bool verbose_equal = 64004; + optional bool face = 64005; + optional bool gostring = 64006; + optional bool populate = 64007; + optional bool stringer = 67008; + optional bool onlyone = 64009; + + optional bool equal = 64013; + optional bool description = 64014; + optional bool testgen = 64015; + optional bool benchgen = 64016; + optional bool marshaler = 64017; + optional bool unmarshaler = 64018; + optional bool stable_marshaler = 64019; + + optional bool sizer = 64020; + + optional bool unsafe_marshaler = 64023; + optional bool unsafe_unmarshaler = 64024; + + optional bool goproto_extensions_map = 64025; + optional bool goproto_unrecognized = 64026; + + optional bool protosizer = 64028; + optional bool compare = 64029; + + optional bool typedecl = 64030; + + optional bool messagename = 64033; + + optional bool goproto_sizecache = 64034; + optional bool goproto_unkeyed = 64035; +} + +extend google.protobuf.FieldOptions { + optional bool nullable = 65001; + optional bool embed = 65002; + optional string customtype = 65003; + optional string customname = 65004; + optional string jsontag = 65005; + optional string moretags = 65006; + optional string casttype = 65007; + optional string castkey = 65008; + optional string castvalue = 65009; + + optional bool stdtime = 65010; + optional bool stdduration = 65011; + optional bool wktpointer = 65012; + + optional string castrepeated = 65013; +} diff --git a/modules/sdk-coin-islm/resources/proto/google/api/annotations.proto b/modules/sdk-coin-islm/resources/proto/google/api/annotations.proto new file mode 100644 index 0000000000..efdab3db6c --- /dev/null +++ b/modules/sdk-coin-islm/resources/proto/google/api/annotations.proto @@ -0,0 +1,31 @@ +// Copyright 2015 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/api/http.proto"; +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.MethodOptions { + // See `HttpRule`. + HttpRule http = 72295728; +} diff --git a/modules/sdk-coin-islm/resources/proto/google/api/http.proto b/modules/sdk-coin-islm/resources/proto/google/api/http.proto new file mode 100644 index 0000000000..113fa936a0 --- /dev/null +++ b/modules/sdk-coin-islm/resources/proto/google/api/http.proto @@ -0,0 +1,375 @@ +// Copyright 2015 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "HttpProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// Defines the HTTP configuration for an API service. It contains a list of +// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +// to one or more HTTP REST API methods. +message Http { + // A list of HTTP configuration rules that apply to individual API methods. + // + // **NOTE:** All service configuration rules follow "last one wins" order. + repeated HttpRule rules = 1; + + // When set to true, URL path parameters will be fully URI-decoded except in + // cases of single segment matches in reserved expansion, where "%2F" will be + // left encoded. + // + // The default behavior is to not decode RFC 6570 reserved characters in multi + // segment matches. + bool fully_decode_reserved_expansion = 2; +} + +// # gRPC Transcoding +// +// gRPC Transcoding is a feature for mapping between a gRPC method and one or +// more HTTP REST endpoints. It allows developers to build a single API service +// that supports both gRPC APIs and REST APIs. Many systems, including [Google +// APIs](https://github.com/googleapis/googleapis), +// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC +// Gateway](https://github.com/grpc-ecosystem/grpc-gateway), +// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature +// and use it for large scale production services. +// +// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies +// how different portions of the gRPC request message are mapped to the URL +// path, URL query parameters, and HTTP request body. It also controls how the +// gRPC response message is mapped to the HTTP response body. `HttpRule` is +// typically specified as an `google.api.http` annotation on the gRPC method. +// +// Each mapping specifies a URL path template and an HTTP method. The path +// template may refer to one or more fields in the gRPC request message, as long +// as each field is a non-repeated field with a primitive (non-message) type. +// The path template controls how fields of the request message are mapped to +// the URL path. +// +// Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/{name=messages/*}" +// }; +// } +// } +// message GetMessageRequest { +// string name = 1; // Mapped to URL path. +// } +// message Message { +// string text = 1; // The resource content. +// } +// +// This enables an HTTP REST to gRPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` +// +// Any fields in the request message which are not bound by the path template +// automatically become HTTP query parameters if there is no HTTP request body. +// For example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get:"/v1/messages/{message_id}" +// }; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // Mapped to URL path. +// int64 revision = 2; // Mapped to URL query parameter `revision`. +// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. +// } +// +// This enables a HTTP JSON to RPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | +// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: +// "foo"))` +// +// Note that fields which are mapped to URL query parameters must have a +// primitive type or a repeated primitive type or a non-repeated message type. +// In the case of a repeated type, the parameter can be repeated in the URL +// as `...?param=A¶m=B`. In the case of a message type, each field of the +// message is mapped to a separate parameter, such as +// `...?foo.a=A&foo.b=B&foo.c=C`. +// +// For HTTP methods that allow a request body, the `body` field +// specifies the mapping. Consider a REST update method on the +// message resource collection: +// +// service Messaging { +// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "message" +// }; +// } +// } +// message UpdateMessageRequest { +// string message_id = 1; // mapped to the URL +// Message message = 2; // mapped to the body +// } +// +// The following HTTP JSON to RPC mapping is enabled, where the +// representation of the JSON in the request body is determined by +// protos JSON encoding: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" message { text: "Hi!" })` +// +// The special name `*` can be used in the body mapping to define that +// every field not bound by the path template should be mapped to the +// request body. This enables the following alternative definition of +// the update method: +// +// service Messaging { +// rpc UpdateMessage(Message) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "*" +// }; +// } +// } +// message Message { +// string message_id = 1; +// string text = 2; +// } +// +// +// The following HTTP JSON to RPC mapping is enabled: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" text: "Hi!")` +// +// Note that when using `*` in the body mapping, it is not possible to +// have HTTP parameters, as all fields not bound by the path end in +// the body. This makes this option more rarely used in practice when +// defining REST APIs. The common usage of `*` is in custom methods +// which don't use the URL at all for transferring data. +// +// It is possible to define multiple HTTP methods for one RPC by using +// the `additional_bindings` option. Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/messages/{message_id}" +// additional_bindings { +// get: "/v1/users/{user_id}/messages/{message_id}" +// } +// }; +// } +// } +// message GetMessageRequest { +// string message_id = 1; +// string user_id = 2; +// } +// +// This enables the following two alternative HTTP JSON to RPC mappings: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` +// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: +// "123456")` +// +// ## Rules for HTTP mapping +// +// 1. Leaf request fields (recursive expansion nested messages in the request +// message) are classified into three categories: +// - Fields referred by the path template. They are passed via the URL path. +// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They are passed via the HTTP +// request body. +// - All other fields are passed via the URL query parameters, and the +// parameter name is the field path in the request message. A repeated +// field can be represented as multiple query parameters under the same +// name. +// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL query parameter, all fields +// are passed via URL path and HTTP request body. +// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP request body, all +// fields are passed via URL path and URL query parameters. +// +// ### Path template syntax +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// The syntax `*` matches a single URL path segment. The syntax `**` matches +// zero or more URL path segments, which must be the last part of the URL path +// except the `Verb`. +// +// The syntax `Variable` matches part of the URL path as specified by its +// template. A variable template must not contain other variables. If a variable +// matches a single path segment, its template may be omitted, e.g. `{var}` +// is equivalent to `{var=*}`. +// +// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` +// contains any reserved character, such characters should be percent-encoded +// before the matching. +// +// If a variable contains exactly one path segment, such as `"{var}"` or +// `"{var=*}"`, when such a variable is expanded into a URL path on the client +// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The +// server side does the reverse decoding. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{var}`. +// +// If a variable contains multiple path segments, such as `"{var=foo/*}"` +// or `"{var=**}"`, when such a variable is expanded into a URL path on the +// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. +// The server side does the reverse decoding, except "%2F" and "%2f" are left +// unchanged. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{+var}`. +// +// ## Using gRPC API Service Configuration +// +// gRPC API Service Configuration (service config) is a configuration language +// for configuring a gRPC service to become a user-facing product. The +// service config is simply the YAML representation of the `google.api.Service` +// proto message. +// +// As an alternative to annotating your proto file, you can configure gRPC +// transcoding in your service config YAML files. You do this by specifying a +// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same +// effect as the proto annotation. This can be particularly useful if you +// have a proto that is reused in multiple services. Note that any transcoding +// specified in the service config will override any matching transcoding +// configuration in the proto. +// +// Example: +// +// http: +// rules: +// # Selects a gRPC method and applies HttpRule to it. +// - selector: example.v1.Messaging.GetMessage +// get: /v1/messages/{message_id}/{sub.subfield} +// +// ## Special notes +// +// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the +// proto to JSON conversion must follow the [proto3 +// specification](https://developers.google.com/protocol-buffers/docs/proto3#json). +// +// While the single segment variable follows the semantics of +// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String +// Expansion, the multi segment variable **does not** follow RFC 6570 Section +// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion +// does not expand special characters like `?` and `#`, which would lead +// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding +// for multi segment variables. +// +// The path variables **must not** refer to any repeated or mapped field, +// because client libraries are not capable of handling such variable expansion. +// +// The path variables **must not** capture the leading "/" character. The reason +// is that the most common use case "{var}" does not capture the leading "/" +// character. For consistency, all path variables must share the same behavior. +// +// Repeated message fields must not be mapped to URL query parameters, because +// no client library can support such complicated mapping. +// +// If an API needs to use a JSON array for request or response body, it can map +// the request or response body to a repeated field. However, some gRPC +// Transcoding implementations may not support this feature. +message HttpRule { + // Selects a method to which this rule applies. + // + // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. + string selector = 1; + + // Determines the URL pattern is matched by this rules. This pattern can be + // used with any of the {get|put|post|delete|patch} methods. A custom method + // can be defined using the 'custom' field. + oneof pattern { + // Maps to HTTP GET. Used for listing and getting information about + // resources. + string get = 2; + + // Maps to HTTP PUT. Used for replacing a resource. + string put = 3; + + // Maps to HTTP POST. Used for creating a resource or performing an action. + string post = 4; + + // Maps to HTTP DELETE. Used for deleting a resource. + string delete = 5; + + // Maps to HTTP PATCH. Used for updating a resource. + string patch = 6; + + // The custom pattern is used for specifying an HTTP method that is not + // included in the `pattern` field, such as HEAD, or "*" to leave the + // HTTP method unspecified for this rule. The wild-card rule is useful + // for services that provide content to Web (HTML) clients. + CustomHttpPattern custom = 8; + } + + // The name of the request field whose value is mapped to the HTTP request + // body, or `*` for mapping all request fields not captured by the path + // pattern to the HTTP body, or omitted for not having any HTTP request body. + // + // NOTE: the referred field must be present at the top-level of the request + // message type. + string body = 7; + + // Optional. The name of the response field whose value is mapped to the HTTP + // response body. When omitted, the entire response message will be used + // as the HTTP response body. + // + // NOTE: The referred field must be present at the top-level of the response + // message type. + string response_body = 12; + + // Additional HTTP bindings for the selector. Nested bindings must + // not contain an `additional_bindings` field themselves (that is, + // the nesting may only be one level deep). + repeated HttpRule additional_bindings = 11; +} + +// A custom pattern is used for defining custom HTTP verb. +message CustomHttpPattern { + // The name of this custom HTTP verb. + string kind = 1; + + // The path matched by this custom verb. + string path = 2; +} diff --git a/modules/sdk-coin-islm/resources/types/ethSecp256k1.ts b/modules/sdk-coin-islm/resources/types/ethSecp256k1.ts new file mode 100644 index 0000000000..3d24d5775a --- /dev/null +++ b/modules/sdk-coin-islm/resources/types/ethSecp256k1.ts @@ -0,0 +1,183 @@ +/* eslint-disable */ +import * as _m0 from 'protobufjs/minimal'; + +export const protobufPackage = 'ethermint.crypto.v1.ethsecp256k1'; + +/** + * PubKey defines a type alias for an ecdsa.PublicKey that implements + * Tendermint's PubKey interface. It represents the 33-byte compressed public + * key format. + */ +export interface PubKey { + /** key is the public key in byte form */ + key: Uint8Array; +} + +/** + * PrivKey defines a type alias for an ecdsa.PrivateKey that implements + * Tendermint's PrivateKey interface. + */ +export interface PrivKey { + /** key is the private key in byte form */ + key: Uint8Array; +} + +function createBasePubKey(): PubKey { + return { key: new Uint8Array(0) }; +} + +export const PubKey = { + encode(message: PubKey, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.key.length !== 0) { + writer.uint32(10).bytes(message.key); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): PubKey { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBasePubKey(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.key = reader.bytes(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): PubKey { + return { key: isSet(object.key) ? bytesFromBase64(object.key) : new Uint8Array(0) }; + }, + + toJSON(message: PubKey): unknown { + const obj: any = {}; + if (message.key.length !== 0) { + obj.key = base64FromBytes(message.key); + } + return obj; + }, + + create, I>>(base?: I): PubKey { + return PubKey.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): PubKey { + const message = createBasePubKey(); + message.key = object.key ?? new Uint8Array(0); + return message; + }, +}; + +function createBasePrivKey(): PrivKey { + return { key: new Uint8Array(0) }; +} + +export const PrivKey = { + encode(message: PrivKey, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.key.length !== 0) { + writer.uint32(10).bytes(message.key); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): PrivKey { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBasePrivKey(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.key = reader.bytes(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): PrivKey { + return { key: isSet(object.key) ? bytesFromBase64(object.key) : new Uint8Array(0) }; + }, + + toJSON(message: PrivKey): unknown { + const obj: any = {}; + if (message.key.length !== 0) { + obj.key = base64FromBytes(message.key); + } + return obj; + }, + + create, I>>(base?: I): PrivKey { + return PrivKey.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): PrivKey { + const message = createBasePrivKey(); + message.key = object.key ?? new Uint8Array(0); + return message; + }, +}; + +function bytesFromBase64(b64: string): Uint8Array { + if (globalThis.Buffer) { + return Uint8Array.from(globalThis.Buffer.from(b64, 'base64')); + } else { + const bin = globalThis.atob(b64); + const arr = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; ++i) { + arr[i] = bin.charCodeAt(i); + } + return arr; + } +} + +function base64FromBytes(arr: Uint8Array): string { + if (globalThis.Buffer) { + return globalThis.Buffer.from(arr).toString('base64'); + } else { + const bin: string[] = []; + arr.forEach((byte) => { + bin.push(globalThis.String.fromCharCode(byte)); + }); + return globalThis.btoa(bin.join('')); + } +} + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin + ? T + : T extends globalThis.Array + ? globalThis.Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; + +type KeysOfUnion = T extends T ? keyof T : never; +export type Exact = P extends Builtin + ? P + : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} diff --git a/modules/sdk-coin-islm/src/index.ts b/modules/sdk-coin-islm/src/index.ts new file mode 100644 index 0000000000..1bd07fdb77 --- /dev/null +++ b/modules/sdk-coin-islm/src/index.ts @@ -0,0 +1,4 @@ +export * from './islm'; +export * from './lib'; +export * from './register'; +export * from './tislm'; diff --git a/modules/sdk-coin-islm/src/islm.ts b/modules/sdk-coin-islm/src/islm.ts new file mode 100644 index 0000000000..2d7583a7b5 --- /dev/null +++ b/modules/sdk-coin-islm/src/islm.ts @@ -0,0 +1,72 @@ +import { CosmosCoin, CosmosKeyPair, GasAmountDetails } from '@bitgo/abstract-cosmos'; +import { BaseCoin, BitGoBase, Environments } from '@bitgo/sdk-core'; +import { BaseCoin as StaticsBaseCoin, BaseUnit, coins } from '@bitgo/statics'; +import { KeyPair, TransactionBuilderFactory } from './lib'; +import { GAS_AMOUNT, GAS_LIMIT } from './lib/constants'; +import utils from './lib/utils'; +import { Hash } from 'crypto'; + +export class Islm 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 Islm(bitgo, staticsCoin); + } + + /** @inheritDoc **/ + getBuilder(): TransactionBuilderFactory { + return new TransactionBuilderFactory(coins.get(this.getChain())); + } + + /** @inheritDoc **/ + getBaseFactor(): string | number { + return 1e18; + } + + /** @inheritDoc **/ + isValidAddress(address: string): boolean { + return utils.isValidAddress(address) || utils.isValidValidatorAddress(address); + } + + /** @inheritDoc **/ + protected getPublicNodeUrl(): string { + return Environments[this.bitgo.getEnv()].islmNodeUrl; + } + + /** @inheritDoc **/ + getDenomination(): string { + return BaseUnit.ISLM; + } + + /** @inheritDoc **/ + getGasAmountDetails(): GasAmountDetails { + return { + gasAmount: GAS_AMOUNT, + gasLimit: GAS_LIMIT, + }; + } + + /** @inheritDoc **/ + getKeyPair(publicKey: string): CosmosKeyPair { + return new KeyPair({ pub: publicKey }); + } + + /** @inheritDoc **/ + getAddressFromPublicKey(publicKey: string): string { + return new KeyPair({ pub: publicKey }).getAddress(); + } + + /** @inheritDoc **/ + getHashFunction(): Hash { + return utils.getHashFunction(); + } +} diff --git a/modules/sdk-coin-islm/src/lib/constants.ts b/modules/sdk-coin-islm/src/lib/constants.ts new file mode 100644 index 0000000000..e92ef1313c --- /dev/null +++ b/modules/sdk-coin-islm/src/lib/constants.ts @@ -0,0 +1,6 @@ +export const validDenoms = ['aISLM']; +export const accountAddressRegex = /^(haqq)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']{38})$/; +export const validatorAddressRegex = /^(haqqvaloper)1(['qpzry9x8gf2tvdw0s3jn54khce6mua7l']{38})$/; +export const GAS_LIMIT = 500000; +export const GAS_AMOUNT = '5000'; +export const ADDRESS_PREFIX = 'haqq'; diff --git a/modules/sdk-coin-islm/src/lib/index.ts b/modules/sdk-coin-islm/src/lib/index.ts new file mode 100644 index 0000000000..87cc346db5 --- /dev/null +++ b/modules/sdk-coin-islm/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-islm/src/lib/keyPair.ts b/modules/sdk-coin-islm/src/lib/keyPair.ts new file mode 100644 index 0000000000..b2ee4b7921 --- /dev/null +++ b/modules/sdk-coin-islm/src/lib/keyPair.ts @@ -0,0 +1,19 @@ +import { KeyPairOptions } from '@bitgo/sdk-core'; +import { CosmosKeyPair } from '@bitgo/abstract-cosmos'; +import { ADDRESS_PREFIX } from './constants'; +import { toBech32, fromHex } from '@cosmjs/encoding'; +import { computeAddress } from 'ethers/lib/utils'; + +/** + * Islm keys and address management. + */ +export class KeyPair extends CosmosKeyPair { + constructor(source?: KeyPairOptions) { + super(source); + } + + /** @inheritdoc */ + getAddress(): string { + return toBech32(ADDRESS_PREFIX, fromHex(computeAddress('0x' + this.getKeys().pub).slice(2))); + } +} diff --git a/modules/sdk-coin-islm/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-islm/src/lib/transactionBuilderFactory.ts new file mode 100644 index 0000000000..4f2c2ac166 --- /dev/null +++ b/modules/sdk-coin-islm/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 islmUtils from './utils'; + +export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { + constructor(_coinConfig: Readonly) { + super(_coinConfig); + } + + /** @inheritdoc */ + from(raw: string): CosmosTransactionBuilder { + const tx = new CosmosTransaction(this._coinConfig, islmUtils); + 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, islmUtils)); + } + + /** @inheritdoc */ + getStakingActivateBuilder(tx?: CosmosTransaction): StakingActivateBuilder { + return this.initializeBuilder(tx, new StakingActivateBuilder(this._coinConfig, islmUtils)); + } + + /** @inheritdoc */ + getStakingDeactivateBuilder(tx?: CosmosTransaction): StakingDeactivateBuilder { + return this.initializeBuilder(tx, new StakingDeactivateBuilder(this._coinConfig, islmUtils)); + } + + /** @inheritdoc */ + getStakingWithdrawRewardsBuilder(tx?: CosmosTransaction): StakingWithdrawRewardsBuilder { + return this.initializeBuilder(tx, new StakingWithdrawRewardsBuilder(this._coinConfig, islmUtils)); + } + + /** @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-islm/src/lib/utils.ts b/modules/sdk-coin-islm/src/lib/utils.ts new file mode 100644 index 0000000000..65b34a0855 --- /dev/null +++ b/modules/sdk-coin-islm/src/lib/utils.ts @@ -0,0 +1,86 @@ +import { InvalidTransactionError } from '@bitgo/sdk-core'; +import { Coin } from '@cosmjs/stargate'; +import BigNumber from 'bignumber.js'; + +import { CosmosUtils, PubKeyType, PubKeyTypeUrl } from '@bitgo/abstract-cosmos'; +import * as constants from './constants'; +import { DecodedTxRaw } from '@cosmjs/proto-signing'; +import { fromBase64, toBase64, toHex, fromHex } from '@cosmjs/encoding'; +import { Pubkey } from '@cosmjs/amino'; +import { Any } from 'cosmjs-types/google/protobuf/any'; +import { PubKey } from '../../resources/types/ethSecp256k1'; +import { Hash } from 'crypto'; +import Keccak from 'keccak'; + +export class IslmUtils 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); + } + } + + /** @inheritdoc */ + getPublicKeyFromDecodedTx(decodedTx: DecodedTxRaw): string | undefined { + const publicKeyUInt8Array = decodedTx.authInfo.signerInfos?.[0].publicKey?.value; + if (publicKeyUInt8Array) { + return toHex(fromBase64(this.decodePubkey(decodedTx.authInfo.signerInfos?.[0].publicKey)?.value)); + } + return undefined; + } + + decodePubkey(pubkey?: Any | null): Pubkey | null { + if (!pubkey || !pubkey.value) { + return null; + } + const { key } = PubKey.decode(pubkey.value); + return this.encodeSecp256k1Pubkey(key); + } + + getEncodedPubkey(pubkey: string): Any { + return this.encodePubkey(this.encodeSecp256k1Pubkey(fromHex(pubkey))); + } + + encodePubkey(pubkey: Pubkey): Any { + const pubkeyProto = PubKey.fromPartial({ + key: fromBase64(pubkey.value), + }); + return Any.fromPartial({ + typeUrl: PubKeyTypeUrl.ethSecp256k1, + value: Uint8Array.from(PubKey.encode(pubkeyProto).finish()), + }); + } + + encodeSecp256k1Pubkey(pubkey: Uint8Array): Pubkey { + if (pubkey.length !== 33 || (pubkey[0] !== 0x02 && pubkey[0] !== 0x03)) { + throw new Error('Public key must be compressed ethSecp256k1, i.e. 33 bytes starting with 0x02 or 0x03'); + } + return { + type: PubKeyType.ethSecp256k1, + value: toBase64(pubkey), + }; + } + + /** @inheritdoc */ + getHashFunction(): Hash { + return Keccak('keccak256'); + } +} + +const islmUtils: CosmosUtils = new IslmUtils(); + +export default islmUtils; diff --git a/modules/sdk-coin-islm/src/register.ts b/modules/sdk-coin-islm/src/register.ts new file mode 100644 index 0000000000..6ee34147e7 --- /dev/null +++ b/modules/sdk-coin-islm/src/register.ts @@ -0,0 +1,8 @@ +import { BitGoBase } from '@bitgo/sdk-core'; +import { Islm } from './islm'; +import { Tislm } from './tislm'; + +export const register = (sdk: BitGoBase): void => { + sdk.register('islm', Islm.createInstance); + sdk.register('tislm', Tislm.createInstance); +}; diff --git a/modules/sdk-coin-islm/src/tislm.ts b/modules/sdk-coin-islm/src/tislm.ts new file mode 100644 index 0000000000..2be3296540 --- /dev/null +++ b/modules/sdk-coin-islm/src/tislm.ts @@ -0,0 +1,25 @@ +/** + * Testnet Islm + * + * @format + */ +import { BaseCoin, BitGoBase } from '@bitgo/sdk-core'; +import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics'; +import { Islm } from './islm'; + +export class Tislm extends Islm { + 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 Tislm(bitgo, staticsCoin); + } +} diff --git a/modules/sdk-coin-islm/test/resources/islm.ts b/modules/sdk-coin-islm/test/resources/islm.ts new file mode 100644 index 0000000000..0b97a77c5e --- /dev/null +++ b/modules/sdk-coin-islm/test/resources/islm.ts @@ -0,0 +1,233 @@ +// Get the test data by running the scripts for the particular coin from coin-sandbox repo. + +export const TEST_ACCOUNT = { + pubAddress: 'haqq1z34mxtp9xd0tv89cfyvc9r4cckfqt3gs2a0xh5', + compressedPublicKey: '029ae62ba5168f4a7e4a48ce9ec85b6a0ba1c7cbb676a95ff993f8b0df74a461df', + compressedPublicKeyTwo: '031dabb2069dfa614b9457ce9fc84c2a7b3b6f5e3cbb57c633380c5f4fcfd1486a', + uncompressedPublicKey: + '049ae62ba5168f4a7e4a48ce9ec85b6a0ba1c7cbb676a95ff993f8b0df74a461df371b136a9be5dbc0e233ad70a733a37beeb41398df69376326c9207368255a6a', + privateKey: '2bc15c7ea4881524469f94e053429d92b89f60d1efb5048258291052ae0f70ff', + extendedPrv: + 'xprv9s21ZrQH143K2xS8TWuFksLBHRRc994e166knSrbtBYVLpXDNkEF1DX45MvAdW97nV9FsrzmPtPCcg8dMxzFBeS2SZ1pWHC2k8xLMuTv673', + extendedPub: + 'xpub661MyMwAqRbcFSWbZYSG81GuqTG6YbnVNK2MaqGDSX5UDcrMvHYVZ1qXvdnt6EZWFXMwerL8ySqJvj91855WUxiRcvDyVTwTYDhJ3JYnC8f', +}; + +export const TEST_SEND_TX = { + hash: '2679D297CD41346707EF8116D1691078199FEB8134D1EECAE5894B618D45AC84', + signature: 'Edqy59rfNqIrgmBcHh4j6vsLIl9MeOldeVy/a1aa0yw3163yQBtqvV9Esn3rpwAWNzN6sBFjB00Of3VYdqoxLQ==', + pubKey: 'AyO0EwzBxj7YzTOmNb9TcIGyEvrdfAlsM2LkyFb6FWzj', + privateKey: '3xn43lnhB0RxwYkJ2vW4UeBPPOcb947WYsYuqsOzXa0=', + signedTxBase64: + 'Co4BCosBChwvY29zbW9zLmJhbmsudjFiZXRhMS5Nc2dTZW5kEmsKK2hhcXExc2V5bXdyOHpzNmM5MnJjZnl6NmpsYTRhNTBkcm13Z3E5dHp2bHoSK2hhcXExOXAycm52emFoeHFoOXlrcmozcnFtamRndjBtejZlZm5qbWR6YTcaDwoFYUlTTE0SBjEwMDAwMBJ8ClkKTwooL2V0aGVybWludC5jcnlwdG8udjEuZXRoc2VjcDI1NmsxLlB1YktleRIjCiEDI7QTDMHGPtjNM6Y1v1NwgbIS+t18CWwzYuTIVvoVbOMSBAoCCAEYGBIfChkKBWFJU0xNEhA0MDAwMDAwMDAwMDAwMDAwEMCaDBpAEdqy59rfNqIrgmBcHh4j6vsLIl9MeOldeVy/a1aa0yw3163yQBtqvV9Esn3rpwAWNzN6sBFjB00Of3VYdqoxLQ==', + sender: 'haqq1seymwr8zs6c92rcfyz6jla4a50drmwgq9tzvlz', + recipient: 'haqq19p2rnvzahxqh9ykrj3rqmjdgv0mz6efnjmdza7', + chainId: 'haqq_54211-3', + accountNumber: 167676, + sequence: 24, + sendAmount: '100000', + feeAmount: '4000000000000000', + sendMessage: { + typeUrl: '/cosmos.bank.v1beta1.MsgSend', + value: { + fromAddress: 'haqq1seymwr8zs6c92rcfyz6jla4a50drmwgq9tzvlz', + toAddress: 'haqq19p2rnvzahxqh9ykrj3rqmjdgv0mz6efnjmdza7', + amount: [ + { + denom: 'aISLM', + amount: '100000', + }, + ], + }, + }, + gasBudget: { + amount: [ + { + denom: 'aISLM', + amount: '4000000000000000', + }, + ], + gasLimit: 200000, + }, +}; + +export const TEST_DELEGATE_TX = { + hash: 'F7E8933B59F514A0EEF66434C2438B639D208435071CC6F09BD07E229A19A262', + signature: 'Bd9eww9Nh2WlH6vI8pPkD667hDXh/ErfjNkESsP5EokkUp/qbTaz0VWRXG6fJSbsY0iWhtonlnZqKCijAYqPgQ==', + pubKey: 'AyO0EwzBxj7YzTOmNb9TcIGyEvrdfAlsM2LkyFb6FWzj', + privateKey: '3xn43lnhB0RxwYkJ2vW4UeBPPOcb947WYsYuqsOzXa0=', + signedTxBase64: + 'CpwBCpkBCiMvY29zbW9zLnN0YWtpbmcudjFiZXRhMS5Nc2dEZWxlZ2F0ZRJyCitoYXFxMXNleW13cjh6czZjOTJyY2Z5ejZqbGE0YTUwZHJtd2dxOXR6dmx6EjJoYXFxdmFsb3BlcjE4eGEyZTJ6NG5kbWdlZjlhN2M3bGowMGV0dXR3MmRhZnl5aGRreBoPCgVhSVNMTRIGMTAwMDAwEnwKWQpPCigvZXRoZXJtaW50LmNyeXB0by52MS5ldGhzZWNwMjU2azEuUHViS2V5EiMKIQMjtBMMwcY+2M0zpjW/U3CBshL63XwJbDNi5MhW+hVs4xIECgIIARgREh8KGQoFYUlTTE0SEDQwMDAwMDAwMDAwMDAwMDAQwJoMGkAF317DD02HZaUfq8jyk+QPrruENeH8St+M2QRKw/kSiSRSn+ptNrPRVZFcbp8lJuxjSJaG2ieWdmooKKMBio+B', + delegator: 'haqq1seymwr8zs6c92rcfyz6jla4a50drmwgq9tzvlz', + validator: 'haqqvaloper18xa2e2z4ndmgef9a7c7lj00etutw2dafyyhdkx', + chainId: 'haqq_54211-3', + accountNumber: 167676, + sequence: 17, + sendAmount: '100000', + feeAmount: '4000000000000000', + sendMessage: { + typeUrl: '/cosmos.staking.v1beta1.MsgDelegate', + value: { + delegatorAddress: 'haqq1seymwr8zs6c92rcfyz6jla4a50drmwgq9tzvlz', + validatorAddress: 'haqqvaloper18xa2e2z4ndmgef9a7c7lj00etutw2dafyyhdkx', + amount: { + denom: 'aISLM', + amount: '100000', + }, + }, + }, + gasBudget: { + amount: [ + { + denom: 'aISLM', + amount: '4000000000000000', + }, + ], + gasLimit: 200000, + }, +}; + +export const TEST_UNDELEGATE_TX = { + hash: 'CCBD92831278103E852E8FB0C038717EA2B122A4C59BFEDC76E2BEE5F4F128BE', + signature: 'fB8iHcGdjlb67Si9D68LK/jKsHPVkm7+Xcv+Hv+akG0RHSVrQvy2XowSFjfTwqt16Je7RWlLvfyfL/wq2Y9M6w==', + pubKey: 'AyO0EwzBxj7YzTOmNb9TcIGyEvrdfAlsM2LkyFb6FWzj', + privateKey: '3xn43lnhB0RxwYkJ2vW4UeBPPOcb947WYsYuqsOzXa0=', + signedTxBase64: + 'Cp4BCpsBCiUvY29zbW9zLnN0YWtpbmcudjFiZXRhMS5Nc2dVbmRlbGVnYXRlEnIKK2hhcXExc2V5bXdyOHpzNmM5MnJjZnl6NmpsYTRhNTBkcm13Z3E5dHp2bHoSMmhhcXF2YWxvcGVyMTh4YTJlMno0bmRtZ2VmOWE3YzdsajAwZXR1dHcyZGFmeXloZGt4Gg8KBWFJU0xNEgYxMDAwMDASfApZCk8KKC9ldGhlcm1pbnQuY3J5cHRvLnYxLmV0aHNlY3AyNTZrMS5QdWJLZXkSIwohAyO0EwzBxj7YzTOmNb9TcIGyEvrdfAlsM2LkyFb6FWzjEgQKAggBGBMSHwoZCgVhSVNMTRIQNDAwMDAwMDAwMDAwMDAwMBDAmgwaQHwfIh3BnY5W+u0ovQ+vCyv4yrBz1ZJu/l3L/h7/mpBtER0la0L8tl6MEhY308KrdeiXu0VpS738ny/8KtmPTOs=', + delegator: 'haqq1seymwr8zs6c92rcfyz6jla4a50drmwgq9tzvlz', + validator: 'haqqvaloper18xa2e2z4ndmgef9a7c7lj00etutw2dafyyhdkx', + chainId: 'haqq_54211-3', + accountNumber: 167676, + sequence: 19, + sendAmount: '100000', + feeAmount: '4000000000000000', + sendMessage: { + typeUrl: '/cosmos.staking.v1beta1.MsgUndelegate', + value: { + delegatorAddress: 'haqq1seymwr8zs6c92rcfyz6jla4a50drmwgq9tzvlz', + validatorAddress: 'haqqvaloper18xa2e2z4ndmgef9a7c7lj00etutw2dafyyhdkx', + amount: { + denom: 'aISLM', + amount: '100000', + }, + }, + }, + gasBudget: { + amount: [ + { + denom: 'aISLM', + amount: '4000000000000000', + }, + ], + gasLimit: 200000, + }, +}; + +export const TEST_WITHDRAW_REWARDS_TX = { + hash: '11CD80A6B569D6EC5D42C0CE082CBD125472A6BF61FC55E500950BD1A9A3B957', + signature: 'KXFbRvLeI+CvphTMZYz0U6wJuZ6VAZApXg/M4N8MWWpLsK8ERzppbgSSWGU30hDuI2GtD+CHUAiYy1bS3mDzSQ==', + pubKey: 'AyO0EwzBxj7YzTOmNb9TcIGyEvrdfAlsM2LkyFb6FWzj', + privateKey: '3xn43lnhB0RxwYkJ2vW4UeBPPOcb947WYsYuqsOzXa0=', + signedTxBase64: + 'Cp8BCpwBCjcvY29zbW9zLmRpc3RyaWJ1dGlvbi52MWJldGExLk1zZ1dpdGhkcmF3RGVsZWdhdG9yUmV3YXJkEmEKK2hhcXExc2V5bXdyOHpzNmM5MnJjZnl6NmpsYTRhNTBkcm13Z3E5dHp2bHoSMmhhcXF2YWxvcGVyMTh4YTJlMno0bmRtZ2VmOWE3YzdsajAwZXR1dHcyZGFmeXloZGt4EnwKWQpPCigvZXRoZXJtaW50LmNyeXB0by52MS5ldGhzZWNwMjU2azEuUHViS2V5EiMKIQMjtBMMwcY+2M0zpjW/U3CBshL63XwJbDNi5MhW+hVs4xIECgIIARgSEh8KGQoFYUlTTE0SEDQwMDAwMDAwMDAwMDAwMDAQwJoMGkApcVtG8t4j4K+mFMxljPRTrAm5npUBkCleD8zg3wxZakuwrwRHOmluBJJYZTfSEO4jYa0P4IdQCJjLVtLeYPNJ', + delegator: 'haqq1seymwr8zs6c92rcfyz6jla4a50drmwgq9tzvlz', + validator: 'haqqvaloper18xa2e2z4ndmgef9a7c7lj00etutw2dafyyhdkx', + chainId: 'haqq_54211-3', + accountNumber: 167676, + sequence: 18, + sendAmount: '100000', + feeAmount: '4000000000000000', + sendMessage: { + typeUrl: '/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward', + value: { + delegatorAddress: 'haqq1seymwr8zs6c92rcfyz6jla4a50drmwgq9tzvlz', + validatorAddress: 'haqqvaloper18xa2e2z4ndmgef9a7c7lj00etutw2dafyyhdkx', + amount: { + denom: 'aISLM', + amount: '100000', + }, + }, + }, + gasBudget: { + amount: [ + { + denom: 'aISLM', + amount: '4000000000000000', + }, + ], + gasLimit: 200000, + }, +}; + +export const TEST_TX_WITH_MEMO = { + hash: 'D930F12BC6764AA1971AFB20F466AC6F598C664BBCFE792D2D421C8C77B239A8', + signature: 'ojuPe7cO37Zs69g5lHSg+NO18WE0/6qNoKnyeaomeYxhp+U7Ym7J5Z/fWAWTrP2+AWRmCqNuPOhnSxfFZabkSw==', + pubKey: 'AyO0EwzBxj7YzTOmNb9TcIGyEvrdfAlsM2LkyFb6FWzj', + privateKey: '3xn43lnhB0RxwYkJ2vW4UeBPPOcb947WYsYuqsOzXa0=', + signedTxBase64: + 'CpIBCosBChwvY29zbW9zLmJhbmsudjFiZXRhMS5Nc2dTZW5kEmsKK2hhcXExc2V5bXdyOHpzNmM5MnJjZnl6NmpsYTRhNTBkcm13Z3E5dHp2bHoSK2hhcXExOXAycm52emFoeHFoOXlrcmozcnFtamRndjBtejZlZm5qbWR6YTcaDwoFYUlTTE0SBjEwMDAwMBICMjMSfApZCk8KKC9ldGhlcm1pbnQuY3J5cHRvLnYxLmV0aHNlY3AyNTZrMS5QdWJLZXkSIwohAyO0EwzBxj7YzTOmNb9TcIGyEvrdfAlsM2LkyFb6FWzjEgQKAggBGBQSHwoZCgVhSVNMTRIQNDAwMDAwMDAwMDAwMDAwMBDAmgwaQKI7j3u3Dt+2bOvYOZR0oPjTtfFhNP+qjaCp8nmqJnmMYaflO2JuyeWf31gFk6z9vgFkZgqjbjzoZ0sXxWWm5Es=', + sender: 'haqq1seymwr8zs6c92rcfyz6jla4a50drmwgq9tzvlz', + recipient: 'haqq19p2rnvzahxqh9ykrj3rqmjdgv0mz6efnjmdza7', + chainId: 'haqq_54211-3', + accountNumber: 167676, + sequence: 20, + sendAmount: '100000', + feeAmount: '4000000000000000', + sendMessage: { + typeUrl: '/cosmos.bank.v1beta1.MsgSend', + value: { + fromAddress: 'haqq1seymwr8zs6c92rcfyz6jla4a50drmwgq9tzvlz', + toAddress: 'haqq19p2rnvzahxqh9ykrj3rqmjdgv0mz6efnjmdza7', + amount: [ + { + denom: 'aISLM', + amount: '100000', + }, + ], + }, + }, + memo: '23', + gasBudget: { + amount: [ + { + denom: 'aISLM', + amount: '4000000000000000', + }, + ], + gasLimit: 200000, + }, +}; + +export const address = { + address1: 'haqq1g3g6nfmqf3f9lmdhf5g84pu7ustw8jt7tvrfzc', + address2: 'haqq1j3hhwl6exe9yjnggy33raj5s3xeskgsljvzagt', + address3: 'haqq2k3hhwl6exe9yjnggy33raj5s3xeskgsljvzagt', + address4: 'haqq1tmm005xgms7qrfm7jwpst3mj9dp8tzdld34jgv', + validatorAddress1: 'haqqvaloper16lp0xpq87cre5z4jkfddq78r5l4vcd7el2jlmj', + validatorAddress2: 'haqqvaloper16gg3wzq8h98zyn9kjp7aw3jwy6pnxslrdhl7zp', + validatorAddress3: 'haqqvaloper35eafuvcrh3c07z4g3pqgq68n3lmsyu5jd9swsy', + validatorAddress4: 'haqqvaloder1p8k6xk94u24vv9dmxu3vkgg43fs3v72grkpjhm', + noMemoIdAddress: 'haqq1g3g6nfmqf3f9lmdhf5g84pu7ustw8jt7tvrfzc', + validMemoIdAddress: 'haqq1g3g6nfmqf3f9lmdhf5g84pu7ustw8jt7tvrfzc?memoId=2', + invalidMemoIdAddress: 'haqq1g3g6nfmqf3f9lmdhf5g84pu7ustw8jt7tvrfzc?memoId=xyz', + multipleMemoIdAddress: 'haqq1g3g6nfmqf3f9lmdhf5g84pu7ustw8jt7tvrfzc?memoId=3&memoId=12', +}; + +export const blockHash = { + hash1: '78009F3F043D3BFDE78D3DB46C98AC6C4D30BE586E8DA2A28FB1FE537DF79265', + hash2: '1CCB5E358CE84FB9FBD77311D25E0621356CF283159EA703455BA72A5CB61F97', +}; + +export const txIds = { + hash1: 'CCDCFAC079BA3833AD3F8EEF3B411C9D8AE2747EF33CA516488A40E522DDD34D', + hash2: 'B51713C6FFD9EBEAFF158498BB6C406AFCC5E1D0423F38073714DC1F54E576F6', + hash3: '74BF02FE620C37EFC242B945B01A21D8E3BDAEDC23617BB0964AFC5BC598042E', +}; + +export const coinAmounts = { + amount1: { amount: '100000', denom: 'aISLM' }, + amount2: { amount: '1000000', denom: 'aISLM' }, + amount3: { amount: '10000000', denom: 'aISLM' }, + amount4: { amount: '-1', denom: 'aISLM' }, + amount5: { amount: '1000000000', denom: 'uISLM' }, +}; diff --git a/modules/sdk-coin-islm/test/unit/islm.ts b/modules/sdk-coin-islm/test/unit/islm.ts new file mode 100644 index 0000000000..58c1558d09 --- /dev/null +++ b/modules/sdk-coin-islm/test/unit/islm.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 { Tislm, Islm } 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/islm'; +import should = require('should'); + +describe('Islm', function () { + let bitgo: TestBitGoAPI; + let basecoin; + before(function () { + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); + bitgo.safeRegister('islm', Islm.createInstance); + bitgo.safeRegister('tislm', Tislm.createInstance); + bitgo.initializeTestVars(); + basecoin = bitgo.coin('tislm'); + }); + + it('should return the right info', function () { + const islm = bitgo.coin('islm'); + const tislm = bitgo.coin('tislm'); + + islm.getChain().should.equal('islm'); + islm.getFamily().should.equal('islm'); + islm.getFullName().should.equal('Islamic Coin'); + islm.getBaseFactor().should.equal(1e18); + + tislm.getChain().should.equal('tislm'); + tislm.getFamily().should.equal('islm'); + tislm.getFullName().should.equal('Testnet Islamic Coin'); + tislm.getBaseFactor().should.equal(1e18); + }); + + 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: 'haqq1g3g6nfmqf3f9lmdhf5g84pu7ustw8jt7tvrfzc?memoId=7', + coinSpecific: { + rootAddress: 'haqq1g3g6nfmqf3f9lmdhf5g84pu7ustw8jt7tvrfzc', + 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: 'UNAVAILABLE', + 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.recipient, + 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(Islm.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-islm/test/unit/keyPair.ts b/modules/sdk-coin-islm/test/unit/keyPair.ts new file mode 100644 index 0000000000..9b659cdf21 --- /dev/null +++ b/modules/sdk-coin-islm/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/islm'; + +describe('Islm 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 islm 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: any) => e.message === 'Unsupported private key' + ); + }); + + it('from an invalid publicKey', () => { + assert.throws( + () => new KeyPair({ pub: '' }), + (e: any) => 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: any) => 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: any) => 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: any) => 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-islm/test/unit/transaction.ts b/modules/sdk-coin-islm/test/unit/transaction.ts new file mode 100644 index 0000000000..a63cdbc952 --- /dev/null +++ b/modules/sdk-coin-islm/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/islm'; + +describe('Islm Transaction', () => { + let tx: CosmosTransaction; + const config = coins.get('tislm'); + + 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: 'tislm', + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.TEST_SEND_TX.sendMessage.value.toAddress, + value: testData.TEST_SEND_TX.sendMessage.value.amount[0].amount, + coin: 'tislm', + }, + ]); + }); + + 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: 'tislm', + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.TEST_SEND_TX.sendMessage.value.toAddress, + value: testData.TEST_SEND_TX.sendMessage.value.amount[0].amount, + coin: 'tislm', + }, + ]); + }); + + 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: 'tislm', + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.TEST_DELEGATE_TX.validator, + value: testData.TEST_DELEGATE_TX.sendMessage.value.amount.amount, + coin: 'tislm', + }, + ]); + }); + + 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: 'tislm', + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.TEST_UNDELEGATE_TX.validator, + value: testData.TEST_UNDELEGATE_TX.sendMessage.value.amount.amount, + coin: 'tislm', + }, + ]); + }); + + 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: 'tislm', + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.TEST_WITHDRAW_REWARDS_TX.validator, + value: 'UNAVAILABLE', + coin: 'tislm', + }, + ]); + }); + + 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-islm/test/unit/transactionBuilder/StakingActivateBuilder.ts b/modules/sdk-coin-islm/test/unit/transactionBuilder/StakingActivateBuilder.ts new file mode 100644 index 0000000000..30817080bd --- /dev/null +++ b/modules/sdk-coin-islm/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 { Islm, Tislm } from '../../../src'; +import * as testData from '../../resources/islm'; + +describe('Islm Delegate txn Builder', () => { + let bitgo: TestBitGoAPI; + let basecoin; + let factory; + let testTx; + before(function () { + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); + bitgo.safeRegister('islm', Islm.createInstance); + bitgo.safeRegister('tislm', Tislm.createInstance); + bitgo.initializeTestVars(); + basecoin = bitgo.coin('tislm'); + 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-islm/test/unit/transactionBuilder/StakingDeactivateBuilder.ts b/modules/sdk-coin-islm/test/unit/transactionBuilder/StakingDeactivateBuilder.ts new file mode 100644 index 0000000000..897ef412d5 --- /dev/null +++ b/modules/sdk-coin-islm/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 { Islm, Tislm } from '../../../src'; +import * as testData from '../../resources/islm'; + +describe('Islm Undelegate txn Builder', () => { + let bitgo: TestBitGoAPI; + let basecoin; + let factory; + let testTx; + before(function () { + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); + bitgo.safeRegister('islm', Islm.createInstance); + bitgo.safeRegister('tislm', Tislm.createInstance); + bitgo.initializeTestVars(); + basecoin = bitgo.coin('tislm'); + 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-islm/test/unit/transactionBuilder/StakingWithdrawRewardsBuilder.ts b/modules/sdk-coin-islm/test/unit/transactionBuilder/StakingWithdrawRewardsBuilder.ts new file mode 100644 index 0000000000..60b2a9808e --- /dev/null +++ b/modules/sdk-coin-islm/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 { Islm, Tislm } from '../../../src'; +import * as testData from '../../resources/islm'; + +describe('Islm WithdrawRewards txn Builder', () => { + let bitgo: TestBitGoAPI; + let basecoin; + let factory; + let testTx; + before(function () { + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); + bitgo.safeRegister('islm', Islm.createInstance); + bitgo.safeRegister('tislm', Tislm.createInstance); + bitgo.initializeTestVars(); + basecoin = bitgo.coin('tislm'); + 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-islm/test/unit/transactionBuilder/transactionBuilder.ts b/modules/sdk-coin-islm/test/unit/transactionBuilder/transactionBuilder.ts new file mode 100644 index 0000000000..c5f07c6480 --- /dev/null +++ b/modules/sdk-coin-islm/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 { Islm, Tislm } from '../../../src'; +import * as testData from '../../resources/islm'; + +describe('Islm Transaction Builder', async () => { + let bitgo: TestBitGoAPI; + let basecoin; + let factory; + before(function () { + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); + bitgo.safeRegister('islm', Islm.createInstance); + bitgo.safeRegister('tislm', Tislm.createInstance); + bitgo.initializeTestVars(); + basecoin = bitgo.coin('tislm'); + 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-islm/test/unit/transactionBuilder/transferBuilder.ts b/modules/sdk-coin-islm/test/unit/transactionBuilder/transferBuilder.ts new file mode 100644 index 0000000000..5b4aa6187a --- /dev/null +++ b/modules/sdk-coin-islm/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 { Islm, Tislm } from '../../../src'; +import * as testData from '../../resources/islm'; + +describe('Islm Transfer Builder', () => { + let bitgo: TestBitGoAPI; + let basecoin; + let factory; + let testTx; + let testTxWithMemo; + before(function () { + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); + bitgo.safeRegister('islm', Islm.createInstance); + bitgo.safeRegister('tislm', Tislm.createInstance); + bitgo.initializeTestVars(); + basecoin = bitgo.coin('tislm'); + 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-islm/test/unit/utils.ts b/modules/sdk-coin-islm/test/unit/utils.ts new file mode 100644 index 0000000000..9cd2cffe2c --- /dev/null +++ b/modules/sdk-coin-islm/test/unit/utils.ts @@ -0,0 +1,47 @@ +import should from 'should'; + +import utils from '../../src/lib/utils'; +import * as testData from '../resources/islm'; +import { blockHash, txIds } from '../resources/islm'; + +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-islm/tsconfig.json b/modules/sdk-coin-islm/tsconfig.json new file mode 100644 index 0000000000..bcc240224e --- /dev/null +++ b/modules/sdk-coin-islm/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/**/*", "resources/**/*"], + "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 240443d488..e30b69e150 100644 --- a/modules/sdk-core/src/bitgo/environments.ts +++ b/modules/sdk-core/src/bitgo/environments.ts @@ -31,6 +31,7 @@ interface EnvironmentTemplate { seiNodeUrl: string; bldNodeUrl: string; zetaNodeUrl: string; + islmNodeUrl: string; dotNodeUrls: string[]; tronNodes: { full: string; @@ -111,6 +112,7 @@ const mainnetBase: EnvironmentTemplate = { seiNodeUrl: 'https://rest.atlantic-2.seinetwork.io', // TODO(BG-78997): Sei is still only in testnet update to mainnet url when it's live bldNodeUrl: 'https://agoric-api.polkachu.com', zetaNodeUrl: 'https://zetachain-athens.blockpi.network/lcd/v1/public', // TODO(WIN-142): update to mainnet url when it's live + islmNodeUrl: 'https://api.islm.io/', dotNodeUrls: ['wss://rpc.polkadot.io'], tronNodes: { full: 'https://api.trongrid.io', @@ -148,6 +150,7 @@ const testnetBase: EnvironmentTemplate = { seiNodeUrl: 'https://rest.atlantic-2.seinetwork.io', bldNodeUrl: 'https://devnet.api.agoric.net', zetaNodeUrl: 'https://rpc.ankr.com/http/zetachain_athens_testnet', + islmNodeUrl: 'https://rpc.testnet.islm.io', dotNodeUrls: ['wss://westend-rpc.polkadot.io'], tronNodes: { full: 'https://api.shasta.trongrid.io', diff --git a/tsconfig.packages.json b/tsconfig.packages.json index 4f2121ed3e..47c831a218 100644 --- a/tsconfig.packages.json +++ b/tsconfig.packages.json @@ -103,6 +103,9 @@ { "path": "./modules/sdk-coin-injective" }, + { + "path": "./modules/sdk-coin-islm" + }, { "path": "./modules/sdk-coin-ltc" },