Skip to content
This repository has been archived by the owner on Dec 14, 2022. It is now read-only.

Commit

Permalink
Feat/amino sign (#9)
Browse files Browse the repository at this point in the history
* finish amino & ledger support for basic messages
  • Loading branch information
ducphamle2 authored Jul 1, 2022
1 parent 83643e1 commit 7115a3c
Show file tree
Hide file tree
Showing 10 changed files with 394 additions and 38 deletions.
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@oraichain/cosmosjs",
"licenses": [],
"version": "0.0.94",
"version": "0.1.0",
"description": "A JavasSript Open Source Library for Oraichain and possibly many other Cosmos network blockchains as well",
"main": "dist/index.js",
"repository": {
Expand Down Expand Up @@ -55,9 +55,12 @@
"@babel/plugin-transform-runtime": "^7.16.10",
"@babel/preset-env": "^7.13.8",
"@babel/register": "^7.13.8",
"@cosmjs/proto-signing": "^0.28.4",
"@cosmjs/proto-signing": "^0.28.10",
"@cosmjs/amino": "^0.28.10",
"@cosmjs/stargate": "^0.28.10",
"babel-loader": "^8.1.0",
"babel-preset-es2015": "^6.24.1",
"jest": "^28.1.2",
"mocha": "^7.1.1",
"webpack": "^4.39",
"webpack-cli": "^3.3"
Expand Down
5 changes: 3 additions & 2 deletions src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { OfflineDirectSigner, Coin } from '@cosmjs/proto-signing';
import { OfflineAminoSigner } from '@cosmjs/amino';
import * as bip32 from 'bip32';
import message from './messages/proto';
export type BroadCastMode = 'BROADCAST_MODE_UNSPECIFIED' | 'BROADCAST_MODE_BLOCK' | 'BROADCAST_MODE_SYNC' | 'BROADCAST_MODE_ASYNC';
Expand Down Expand Up @@ -33,15 +34,15 @@ declare class Cosmos {
constructBodyBytes(msgAny: any, memo: String): Uint8Array;
constructTxBody(body: { messages: any[], memo?: string, timeout_height?: number }): message.cosmos.tx.v1beta1.TxBody;
constructAuthInfoBytes(pubKeyAny: message.google.protobuf.Any, gas: number, fees: number, sequence: number): Uint8Array
constructTxBytes(bodyBytes: Uint8Array, authInfoBytes: Uint8Array, signatures: Uint8Array[]): Uint8Array
constructSignedTxBytes(bodyBytes: Uint8Array, authInfoBytes: Uint8Array, signatures: Uint8Array[]): Uint8Array
getPubKeyAnyWithPub(pubKeyBytes: Uint8Array): message.google.protobuf.Any;
getAccounts(address: string): Promise<any>;
signRaw(message: Buffer, privKey: Uint8Array): Uint8Array;
// sign(bodyBytes: Uint8Array, authInfoBytes: Uint8Array, accountNumber: any, privKey: Uint8Array): Uint8Array;
getWalletInfoFromSignerOrChild(signerOrChild: bip32.BIP32Interface | OfflineDirectSigner): Promise<{ address: string, pubkey: Uint8Array }>;
sign(signerOrChild: bip32.BIP32Interface | OfflineDirectSigner, bodyBytes: Uint8Array, authInfoBytes: Uint8Array, accountNumber: number, address: string): Promise<Uint8Array>;
broadcast(signedTxBytes: any, broadCastMode?: BroadCastMode): Promise<any>;
submit(signerOrChild: bip32.BIP32Interface | OfflineDirectSigner, txBody: message.cosmos.tx.v1beta1.TxBody, broadCastMode?: BroadCastMode, fees?: Coin[], gas_limit?: number, timeoutIntervalCheck?: number): Promise<any>;
submit(signerOrChild: bip32.BIP32Interface | OfflineDirectSigner | OfflineAminoSigner, txBody: message.cosmos.tx.v1beta1.TxBody, broadCastMode?: BroadCastMode, fees?: Coin[], gas_limit?: number, timeoutIntervalCheck?: number, isAmino?: boolean): Promise<any>;
simulate(publicKey: Buffer, txBody: message.cosmos.tx.v1beta1.TxBody): Promise<any>;
}
declare namespace Cosmos {
Expand Down
71 changes: 46 additions & 25 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import bech32 from 'bech32';
import secp256k1 from 'secp256k1';
import message from './messages/proto';
import CONSTANTS from './constants';
import { isOfflineDirectSigner } from '@cosmjs/proto-signing';
import { trimBuffer, hash160 } from './utils';
import WalletFactory from './wallet/walletFactory';
import WalletSigner from './wallet/walletSigner';
import AminoTypes from './messages/amino';

export default class Cosmos {
constructor(url, chainId, bech32MainPrefix = "orai", hdPath = "m/44'/118'/0'/0/0") {
// strip / at end
Expand Down Expand Up @@ -143,12 +145,12 @@ export default class Cosmos {
return pubKeyAny;
}

constructAuthInfoBytes(pubKeyAny, gas_limit = 200000, fees = [{ denom: 'orai', amount: String(0) }], sequence) {
constructAuthInfoBytes(pubKeyAny, gas_limit = 200000, fees = [{ denom: 'orai', amount: String(0) }], sequence, signMode = 1) {
const signerInfo = new message.cosmos.tx.v1beta1.SignerInfo({
public_key: pubKeyAny,
mode_info: {
single: {
mode: message.cosmos.tx.signing.v1beta1.SignMode.SIGN_MODE_DIRECT
mode: signMode
}
},
sequence
Expand Down Expand Up @@ -198,7 +200,7 @@ export default class Cosmos {
return secp256k1.ecdsaSign(message, privKey).signature;
}

handleFetchResponse = async (response) => {
async handleFetchResponse(response) {
const contentType = response.headers.get("content-type");
if (contentType && contentType.indexOf("application/json") !== -1) {
return response.json();
Expand Down Expand Up @@ -226,45 +228,64 @@ export default class Cosmos {
return this.post('/cosmos/tx/v1beta1/txs', { tx_bytes: txBytesBase64, mode: broadCastMode });
}

async handleBroadcast(signedTxBytes, broadCastMode, timeoutHeight, timeoutIntervalCheck) {
if (!timeoutHeight) {
const res = await this.broadcast(signedTxBytes, broadCastMode);
return this.handleTxResult(res);
}
// use broadcast mode async to collect tx hash
const res = await this.broadcast(signedTxBytes, 'BROADCAST_MODE_SYNC');
// error that is not related to gas fees
if (res.tx_response.code !== 0) return this.handleTxResult(res);
const txHash = res.tx_response.txhash;
const txResult = await this.handleTxTimeout(txHash, timeoutHeight, timeoutIntervalCheck);
return this.handleTxResult(txResult);
}

async getWalletInfoFromSignerOrChild(signerOrChild) {
const wallet = new WalletFactory(signerOrChild, this).wallet;
return wallet.getWalletInfo();
}

async getSignerData(address) {
const data = await this.getAccounts(address);
if (data.code) {
if (data.code === 2) throw { status: CONSTANTS.STATUS_CODE.NOT_FOUND, message: `The wallet address ${address} does not exist` };
else throw { status: CONSTANTS.STATUS_CODE.GENERIC_ERROR, message: data.message ? data.message : `Unexpected error from the network: ${data}` };
}
return data;
}

async sign(signerOrChild, bodyBytes, authInfoBytes, accountNumber, sender) {
const wallet = new WalletFactory(signerOrChild, this).wallet;
const { wallet } = new WalletFactory(signerOrChild, this);
return wallet.sign(bodyBytes, authInfoBytes, accountNumber, sender);
}

async submit(signerOrChild, txBody, broadCastMode = 'BROADCAST_MODE_SYNC', fees = [{ denom: 'orai', amount: String(0) }], gas_limit = 200000, timeoutIntervalCheck = 5000) {
async submit(signerOrChild, txBody, broadCastMode = 'BROADCAST_MODE_SYNC', fees = [{ denom: 'orai', amount: String(0) }], gas_limit = 200000, timeoutIntervalCheck = 0, isAmino = false) {
if (!txBody) throw { status: CONSTANTS.STATUS_CODE.NOT_FOUND, message: "The txBody are empty" };

// collect wallet & signer data
const { wallet } = new WalletFactory(signerOrChild, this);

const { address, pubkey } = await wallet.getWalletInfo();
// simple tx body filter
if (!txBody) throw { status: CONSTANTS.STATUS_CODE.NOT_FOUND, message: "The txBody object is empty" };
const data = await this.getSignerData(address);
const pubKeyAny = this.getPubKeyAnyWithPub(pubkey);
const data = await this.getAccounts(address);
if (data.code) {
if (data.code === 2) throw { status: CONSTANTS.STATUS_CODE.NOT_FOUND, message: `The wallet address ${address} does not exist` };
else throw { status: CONSTANTS.STATUS_CODE.GENERIC_ERROR, message: data.message ? data.message : `Unexpected error from the network: ${data}` };
}

// generate body & auth bytes to prepare broadcasting
const authInfoBytes = this.constructAuthInfoBytes(pubKeyAny, gas_limit, fees, data.account.sequence);
const bodyBytes = message.cosmos.tx.v1beta1.TxBody.encode(txBody).finish();
const signedTxBytes = await wallet.sign(bodyBytes, authInfoBytes, data.account.account_number, address);
var signedTxBytes;

if (!txBody.timeout_height) {
const res = await this.broadcast(signedTxBytes, broadCastMode);
return this.handleTxResult(res);
// filter signer type
if (isAmino || (wallet instanceof WalletSigner && !signerOrChild.signDirect)) {
const aminoType = new AminoTypes();
const aminoMsgs = txBody.messages.map(msg => aminoType.toAmino(msg));
signedTxBytes = await wallet.signAmino(aminoMsgs, bodyBytes, authInfoBytes, data.account.account_number, data.account.sequence, { amount: fees, gas: gas_limit.toString() }, txBody.memo, address);
}
// use broadcast mode async to collect tx hash
const res = await this.broadcast(signedTxBytes, 'BROADCAST_MODE_SYNC');
// error that is not related to gas fees
if (res.tx_response.code !== 0) return this.handleTxResult(res);
const txHash = res.tx_response.txhash;
const txResult = await this.handleTxTimeout(txHash, txBody.timeout_height, timeoutIntervalCheck);
return this.handleTxResult(txResult);
else {
signedTxBytes = await wallet.signDirect(bodyBytes, authInfoBytes, data.account.account_number, address);
}

return this.handleBroadcast(signedTxBytes, broadCastMode, txBody.timeout_height, timeoutIntervalCheck);
}

handleTxResult(res) {
Expand Down
195 changes: 195 additions & 0 deletions src/messages/amino.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import message from './proto';

function createDefaultTypes(prefix) {
return {
"/cosmos.bank.v1beta1.MsgSend": {
aminoType: "cosmos-sdk/MsgSend",
toAmino: (msgSendAny) => {
const msgSend = message.cosmos.bank.v1beta1.MsgSend.decode(msgSendAny);
return {
amount: [...msgSend.amount],
from_address: msgSend.from_address,
to_address: msgSend.to_address,
}
},
fromAmino: ({ from_address, to_address, amount }) => {
const msgSend = new message.cosmos.bank.v1beta1.MsgSend({
from_address,
to_address,
amount
});
return message.cosmos.bank.v1beta1.MsgSend.encode(msgSend).finish();
},
},
"/cosmos.bank.v1beta1.MsgMultiSend": {
aminoType: "cosmos-sdk/MsgMultiSend",
toAmino: (msgMultisendAny) => {
const { inputs, outputs } = message.cosmos.bank.v1beta1.MsgMultiSend.decode(msgMultisendAny);
return {
inputs,
outputs,
}
},
fromAmino: ({ inputs, outputs }) => {
const msgMultiSend = new message.cosmos.bank.v1beta1.MsgMultiSend({
inputs,
outputs
});
return message.cosmos.bank.v1beta1.MsgMultiSend.encode(msgMultiSend).finish();
},
},
// "/cosmos.distribution.v1beta1.MsgFundCommunityPool": {
// aminoType: "cosmos-sdk/MsgFundCommunityPool",
// toAmino: ({ amount, depositor }) => ({
// amount: [...amount],
// depositor: depositor,
// }),
// fromAmino: ({ amount, depositor }) => ({
// amount: [...amount],
// depositor: depositor,
// }),
// },
// "/cosmos.distribution.v1beta1.MsgSetWithdrawAddress": {
// aminoType: "cosmos-sdk/MsgModifyWithdrawAddress",
// toAmino: ({ delegatorAddress, withdrawAddress, }) => ({
// delegator_address: delegatorAddress,
// withdraw_address: withdrawAddress,
// }),
// fromAmino: ({ delegator_address, withdraw_address, }) => ({
// delegatorAddress: delegator_address,
// withdrawAddress: withdraw_address,
// }),
// },
"/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward": {
aminoType: "cosmos-sdk/MsgWithdrawDelegationReward",
toAmino: (msgWithdrawDelegationRewardAny) => {
const { delegator_address, validator_address } = message.cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward.decode(msgWithdrawDelegationRewardAny);
return {
delegator_address,
validator_address,
}
},
fromAmino: ({ delegator_address, validator_address, }) => {
const msgWithdraw = new message.cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward({ delegator_address, validator_address });
return message.cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward.encode(msgWithdraw).finish();

},
},
"/cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission": {
aminoType: "cosmos-sdk/MsgWithdrawValidatorCommission",
toAmino: (msgWithdrawValidatorCommission) => {
const { validator_address } = message.cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission.decode(msgWithdrawValidatorCommission);
return {
validator_address,
}
},
fromAmino: ({ validator_address, }) => {
const msgWithdraw = new message.cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission({ validator_address });
return message.cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission.encode(msgWithdraw).finish();
},
},
"/cosmos.staking.v1beta1.MsgBeginRedelegate": {
aminoType: "cosmos-sdk/MsgBeginRedelegate",
toAmino: (msgRedelegateAny) => {
const msgRedelegate = message.cosmos.staking.v1beta1.MsgBeginRedelegate.decode(msgRedelegateAny);
return { ...msgRedelegate };
},
fromAmino: (data) => {
const msgRedelegate = new message.cosmos.staking.v1beta1.MsgBeginRedelegate(data);
return message.cosmos.staking.v1beta1.MsgBeginRedelegate.encode(msgRedelegate).finish();
},
},
"/cosmos.staking.v1beta1.MsgDelegate": {
aminoType: "cosmos-sdk/MsgDelegate",
toAmino: (msgDelegateAny) => {
const msgDelegate = message.cosmos.staking.v1beta1.MsgDelegate.decode(msgDelegateAny);
return { ...msgDelegate };
},
fromAmino: (data) => {
const msgDelegate = new message.cosmos.staking.v1beta1.MsgDelegate(data);
return message.cosmos.staking.v1beta1.MsgDelegate.encode(msgDelegate).finish();
},
},
"/cosmos.staking.v1beta1.MsgUndelegate": {
aminoType: "cosmos-sdk/MsgUndelegate",
toAmino: (msgUndelegateAny) => {
const msgUndelegate = message.cosmos.staking.v1beta1.MsgUndelegate.decode(msgUndelegateAny);
return { ...msgUndelegate };
},
fromAmino: (data) => {
const msgUndelegate = new message.cosmos.staking.v1beta1.MsgUndelegate(data);
return message.cosmos.staking.v1beta1.MsgUndelegate.encode(msgUndelegate).finish();
},
},
// "/ibc.applications.transfer.v1.MsgTransfer": {
// aminoType: "cosmos-sdk/MsgTransfer",
// toAmino: ({ sourcePort, sourceChannel, token, sender, receiver, timeoutHeight, timeoutTimestamp, }) => {
// var _a, _b, _c;
// return ({
// source_port: sourcePort,
// source_channel: sourceChannel,
// token: token,
// sender: sender,
// receiver: receiver,
// timeout_height: timeoutHeight
// ? {
// revision_height: (_a = omitDefault(timeoutHeight.revisionHeight)) === null || _a === void 0 ? void 0 : _a.toString(),
// revision_number: (_b = omitDefault(timeoutHeight.revisionNumber)) === null || _b === void 0 ? void 0 : _b.toString(),
// }
// : {},
// timeout_timestamp: (_c = omitDefault(timeoutTimestamp)) === null || _c === void 0 ? void 0 : _c.toString(),
// });
// },
// fromAmino: ({ source_port, source_channel, token, sender, receiver, timeout_height, timeout_timestamp, }) => ({
// sourcePort: source_port,
// sourceChannel: source_channel,
// token: token,
// sender: sender,
// receiver: receiver,
// timeoutHeight: timeout_height
// ? {
// revisionHeight: long_1.default.fromString(timeout_height.revision_height || "0", true),
// revisionNumber: long_1.default.fromString(timeout_height.revision_number || "0", true),
// }
// : undefined,
// timeoutTimestamp: long_1.default.fromString(timeout_timestamp || "0", true),
// }),
// },
};
}

export default class AminoTypes {
constructor({ additions = {}, prefix = "orai" } = {}) {
const additionalAminoTypes = Object.values(additions);
const filteredDefaultTypes = Object.entries(createDefaultTypes(prefix)).reduce((acc, [key, value]) => additionalAminoTypes.find(({ aminoType }) => value.aminoType === aminoType)
? acc
: Object.assign(Object.assign({}, acc), { [key]: value }), {});
this.register = Object.assign(Object.assign({}, filteredDefaultTypes), additions);
}
toAmino({ type_url, value }) {
const converter = this.register[type_url];
console.log("converter: ", converter)
if (!converter) {
throw new Error("Type URL does not exist in the Amino message type register. " +
"If you need support for this message type, you can pass in additional entries to the AminoTypes constructor. " +
"If you think this message type should be included by default, please open an issue at https://github.com/cosmos/cosmjs/issues.");
}
return {
type: converter.aminoType,
value: converter.toAmino(value),
};
}
fromAmino({ type, value }) {
const result = Object.entries(this.register).find(([_typeUrl, { aminoType }]) => aminoType === type);
if (!result) {
throw new Error("Type does not exist in the Amino message type register. " +
"If you need support for this message type, you can pass in additional entries to the AminoTypes constructor. " +
"If you think this message type should be included by default, please open an issue at https://github.com/cosmos/cosmjs/issues.");
}
const [type_url, converter] = result;
return {
type_url: type_url,
value: converter.fromAmino(value),
};
}
}
Loading

0 comments on commit 7115a3c

Please sign in to comment.