diff --git a/package-lock.json b/package-lock.json index 3f061dd..6ec16bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42261,7 +42261,7 @@ }, "packages/sdk": { "name": "@swisstronik/sdk", - "version": "1.6.0", + "version": "1.7.0", "license": "Apache-2.0", "dependencies": { "@cosmjs/amino": "^0.31.1", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 2277be6..584aa32 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@swisstronik/sdk", - "version": "1.6.0", + "version": "1.7.0", "description": "TypeScript SDK for Swisstronik Network", "license": "Apache-2.0", "source": "src/index.ts", diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index 1a84012..f92cb75 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -12,6 +12,7 @@ import { Pubkey, SinglePubkey, encodeEd25519Pubkey, + MultisigThresholdPubkey, } from "@cosmjs/amino"; import { Any } from "./types-proto/google/protobuf/any.js"; import Long from "long"; @@ -19,6 +20,7 @@ import { PubKey as CosmosCryptoEd25519Pubkey } from "cosmjs-types/cosmos/crypto/ import { PubKey as CosmosCryptoSecp256k1Pubkey } from "cosmjs-types/cosmos/crypto/secp256k1/keys.js"; import { PubKey as CommonPubKey } from "cosmjs-types/cosmos/crypto/secp256k1/keys.js"; import { Secp256k1 } from "./compatability/secp256k1.js"; +import { LegacyAminoPubKey } from "cosmjs-types/cosmos/crypto/multisig/keys.js"; export class SwisstronikStargateClient extends StargateClient { private readonly overridenAccountParser: AccountParser; @@ -92,17 +94,32 @@ export class SwisstronikStargateClient extends StargateClient { return Uint64.fromString(input.toString()); } - private decodePubkey(pubkey: Any): Pubkey { - switch (pubkey.typeUrl) { - case "/ethermint.crypto.v1.ethsecp256k1.PubKey": - case "/cosmos.crypto.secp256k1.PubKey": - case "/cosmos.crypto.ed25519.PubKey": { - return this.anyToSinglePubkey(pubkey); - } - default: - throw new Error(`Pubkey type_url ${pubkey.typeUrl} not recognized`); - } - } + private decodePubkey(pubkey: Any): Pubkey { + switch (pubkey.typeUrl) { + case "/ethermint.crypto.v1.ethsecp256k1.PubKey": + case "/cosmos.crypto.secp256k1.PubKey": + case "/cosmos.crypto.ed25519.PubKey": { + return this.anyToSinglePubkey(pubkey); + } + case "/cosmos.crypto.multisig.LegacyAminoPubKey": { + return this.anyToMultiPubkey(pubkey); + } + default: + throw new Error(`Pubkey type_url ${pubkey.typeUrl} not recognized`); + } + } + + private anyToMultiPubkey(pubkey: Any): MultisigThresholdPubkey { + const { publicKeys, threshold } = LegacyAminoPubKey.decode(pubkey.value); + const keys = publicKeys.map((key) => this.anyToSinglePubkey(key)); + return { + type: "tendermint/PubKeyMultisigThreshold", + value: { + pubkeys: keys, + threshold: String(threshold) + }, + }; + } private anyToSinglePubkey(pubkey: Any): SinglePubkey { switch (pubkey.typeUrl) { diff --git a/packages/sdk/src/compatability/address.ts b/packages/sdk/src/compatability/address.ts new file mode 100644 index 0000000..9ee7070 --- /dev/null +++ b/packages/sdk/src/compatability/address.ts @@ -0,0 +1,55 @@ +import { fromBase64, fromHex, toBech32 } from "@cosmjs/encoding" +import { Pubkey, isEd25519Pubkey, isMultisigThresholdPubkey, isSecp256k1Pubkey } from "@cosmjs/amino" +import { Uint53 } from "@cosmjs/math" +import { rawSecp256k1PubkeyToRawAddress } from "./secp256k1"; +import { sha256 } from '@cosmjs/crypto' + +export function encodeAminoPubkey(pubkey: Pubkey) { + const pubkeyAminoPrefixMultisigThreshold = fromHex("22c1f7e2" /* variable length not included */); + const pubkeyAminoPrefixSecp256k1 = fromHex("21" /* fixed length */); + const pubkeyAminoPrefixEd25519 = fromHex("1624de64" + "20" /* fixed length */); + + + function encodeUvarint(value: number | string) { + const checked = Uint53.fromString(value.toString()).toNumber(); + if (checked > 127) { + throw new Error("Encoding numbers > 127 is not supported here. Please tell those lazy CosmJS maintainers to port the binary.PutUvarint implementation from the Go standard library and write some tests."); + } + return [checked]; + } + + if (isMultisigThresholdPubkey(pubkey)) { + const out = Array.from(pubkeyAminoPrefixMultisigThreshold); + out.push(0x08); // https://github.com/cosmos/cosmjs/blob/v0.31.1/packages/amino/src/encoding.ts#L198 + out.push(...encodeUvarint(pubkey.value.threshold)); + for (const pubkeyData of pubkey.value.pubkeys.map((p) => encodeAminoPubkey(p))) { + out.push(0x12); // https://github.com/cosmos/cosmjs/blob/v0.31.1/packages/amino/src/encoding.ts#L201 + out.push(...encodeUvarint(pubkeyData.length)); + out.push(...pubkeyData); + } + return new Uint8Array(out); + } + else if (isEd25519Pubkey(pubkey)) { + return new Uint8Array([...pubkeyAminoPrefixEd25519, ...fromBase64(pubkey.value)]); + } + else if (isSecp256k1Pubkey(pubkey)) { + return new Uint8Array([...pubkeyAminoPrefixSecp256k1, ...fromBase64(pubkey.value)]); + } + else { + throw new Error("Unsupported pubkey type"); + } +} + +export function pubkeyToAddress(pubkey: Pubkey) { + if(isSecp256k1Pubkey(pubkey)) { + const buf = Buffer.from(pubkey.value, "base64"); + const bytes = Uint8Array.from(buf); + return toBech32('swtr', rawSecp256k1PubkeyToRawAddress(bytes)); + } else if(isMultisigThresholdPubkey(pubkey)) { + const pubkeyData = encodeAminoPubkey(pubkey); + return toBech32('swtr', sha256(pubkeyData).slice(0, 20)); + } + else { + throw new Error("Unsupported public key type"); + } +} \ No newline at end of file diff --git a/packages/sdk/src/compatability/index.ts b/packages/sdk/src/compatability/index.ts index 595fa03..5acd438 100644 --- a/packages/sdk/src/compatability/index.ts +++ b/packages/sdk/src/compatability/index.ts @@ -1,4 +1,6 @@ export * from './directsecp256k1hdwallet.js' export * from './directsecp256k1wallet.js' export * from './secp256k1.js' -export * from './wallet.js' \ No newline at end of file +export * from './wallet.js' +export * from './address.js' +export * from './multisignature.js' \ No newline at end of file diff --git a/packages/sdk/src/compatability/multisignature.ts b/packages/sdk/src/compatability/multisignature.ts new file mode 100644 index 0000000..acdec5a --- /dev/null +++ b/packages/sdk/src/compatability/multisignature.ts @@ -0,0 +1,123 @@ +import { pubkeyToAddress } from "./address"; +import { + StdFee, + isSecp256k1Pubkey, + isMultisigThresholdPubkey, + MultisigThresholdPubkey, + isEd25519Pubkey, + Pubkey, +} from "@cosmjs/amino"; +import { PubKey as Secp256k1PubKey } from "cosmjs-types/cosmos/crypto/secp256k1/keys.js"; +import { PubKey as Ed25519PubKey } from "cosmjs-types/cosmos/crypto/ed25519/keys.js"; +import { fromBase64 } from "@cosmjs/encoding"; +import { LegacyAminoPubKey } from "cosmjs-types/cosmos/crypto/multisig/keys"; +import { Uint53 } from "@cosmjs/math"; +import { Any } from "../types-proto/google/protobuf/any.js"; +import { makeCompactBitArray } from "@cosmjs/stargate/build/multisignature.js"; +import { SignMode } from "cosmjs-types/cosmos/tx/signing/v1beta1/signing.js"; +import Long from "long"; +import { AuthInfo, TxRaw } from "cosmjs-types/cosmos/tx/v1beta1/tx.js"; +import { MultiSignature } from "cosmjs-types/cosmos/crypto/multisig/v1beta1/multisig.js"; + +export function makeMultisignedTxBytes( + multisigPubkey: MultisigThresholdPubkey, + sequence: number, + fee: StdFee, + bodyBytes: Uint8Array, + signatures: Map +): Uint8Array { + const signedTx = makeMultisignedTx( + multisigPubkey, + sequence, + fee, + bodyBytes, + signatures + ); + return TxRaw.encode(signedTx).finish(); +} + +export function makeMultisignedTx( + multisigPubkey: MultisigThresholdPubkey, + sequence: number, + fee: StdFee, + bodyBytes: Uint8Array, + signatures: Map +) { + const signers = Array(multisigPubkey.value.pubkeys.length).fill(false); + const signaturesList = new Array(); + for (let i = 0; i < multisigPubkey.value.pubkeys.length; i++) { + const signerAddress = pubkeyToAddress(multisigPubkey.value.pubkeys[i]); + const signature = signatures.get(signerAddress); + if (signature) { + signers[i] = true; + signaturesList.push(signature); + } + } + const signerInfo = { + publicKey: encodePubkey(multisigPubkey), + modeInfo: { + multi: { + bitarray: makeCompactBitArray(signers), + modeInfos: signaturesList.map((_) => ({ + single: { mode: SignMode.SIGN_MODE_LEGACY_AMINO_JSON }, + })), + }, + }, + sequence: Long.fromNumber(sequence), + } as any; + + const authInfo = AuthInfo.fromPartial({ + signerInfos: [signerInfo], + fee: { + amount: [...fee.amount], + gasLimit: Long.fromNumber(+fee.gas) as any, + }, + }); + + const authInfoBytes = AuthInfo.encode(authInfo).finish(); + const signedTx = TxRaw.fromPartial({ + bodyBytes: bodyBytes, + authInfoBytes: authInfoBytes, + signatures: [ + MultiSignature.encode( + MultiSignature.fromPartial({ signatures: signaturesList }) + ).finish(), + ], + }); + return signedTx; +} + +function encodePubkey(pubkey: Pubkey) { + if (isSecp256k1Pubkey(pubkey)) { + const pubkeyProto = Secp256k1PubKey.fromPartial({ + key: fromBase64(pubkey.value), + }); + + const anyPubkey = Any.fromPartial({ + typeUrl: "/ethermint.crypto.v1.ethsecp256k1.PubKey", + value: Secp256k1PubKey.encode(pubkeyProto).finish(), + }); + + return anyPubkey; + } else if (isEd25519Pubkey(pubkey)) { + const pubkeyProto = Ed25519PubKey.fromPartial({ + key: fromBase64(pubkey.value), + }); + return Any.fromPartial({ + typeUrl: "/cosmos.crypto.ed25519.PubKey", + value: Uint8Array.from(Ed25519PubKey.encode(pubkeyProto).finish()), + }); + } else if (isMultisigThresholdPubkey(pubkey)) { + const pubkeyProto = LegacyAminoPubKey.fromPartial({ + threshold: Uint53.fromString(pubkey.value.threshold).toNumber(), + publicKeys: pubkey.value.pubkeys.map(encodePubkey), + }) as any; + + return Any.fromPartial({ + typeUrl: "/cosmos.crypto.multisig.LegacyAminoPubKey", + value: Uint8Array.from(LegacyAminoPubKey.encode(pubkeyProto).finish()), + }); + } else { + throw new Error(`Pubkey type ${pubkey.type} not recognized`); + } +} diff --git a/packages/sdk/src/signer.ts b/packages/sdk/src/signer.ts index 7814ad8..07d24de 100644 --- a/packages/sdk/src/signer.ts +++ b/packages/sdk/src/signer.ts @@ -3,6 +3,7 @@ import { isOfflineDirectSigner, OfflineSigner, makeSignDoc, + makeAuthInfoBytes, } from "@cosmjs/proto-signing" import { Uint64 } from "@cosmjs/math"; import { @@ -15,7 +16,9 @@ import { Account, AccountParser, SequenceResponse, - SignerData + SignerData, + AminoTypes, + createDefaultAminoConverters } from "@cosmjs/stargate" import { EthAccount } from "./types-proto/ethermint/types/v1/account.js"; import { Tendermint34Client, Tendermint37Client } from "@cosmjs/tendermint-rpc" @@ -45,7 +48,7 @@ import { assert, assertDefined } from '@cosmjs/utils' -import { encodeSecp256k1Pubkey, Pubkey, SinglePubkey, encodeEd25519Pubkey, StdFee } from '@cosmjs/amino' +import { encodeSecp256k1Pubkey, Pubkey, SinglePubkey, encodeEd25519Pubkey, StdFee, makeSignDoc as makeSignDocAmino, isSecp256k1Pubkey, isMultisigThresholdPubkey, MultisigThresholdPubkey } from '@cosmjs/amino' import { Int53 } from '@cosmjs/math' import { fromBase64 } from '@cosmjs/encoding' import { @@ -61,6 +64,7 @@ import { PubKey as CosmosCryptoEd25519Pubkey } from "cosmjs-types/cosmos/crypto/ import { PubKey as CosmosCryptoSecp256k1Pubkey } from "cosmjs-types/cosmos/crypto/secp256k1/keys.js"; import { PubKey as CommonPubKey } from "cosmjs-types/cosmos/crypto/secp256k1/keys.js"; import { Secp256k1 } from "./compatability/secp256k1.js"; +import {LegacyAminoPubKey} from "cosmjs-types/cosmos/crypto/multisig/keys" export function calculateDidFee(gasLimit: number, gasPrice: string | GasPrice): DidStdFee { return calculateFee(gasLimit, gasPrice) @@ -103,6 +107,7 @@ export class SwisstronikSigningStargateClient extends SigningStargateClient { private didSigners: TSignerAlgo = {} private readonly _gasPrice: GasPrice | undefined private readonly _signer: OfflineSigner + private readonly _aminoTypes: AminoTypes; private readonly overridenAccountParser: AccountParser; @@ -128,6 +133,14 @@ export class SwisstronikSigningStargateClient extends SigningStargateClient { }) } + static async offline(signer: OfflineSigner, options: SigningStargateClientOptions | undefined = {}) { + console.log(`[sdk::signer.ts] Creating offline signer`); + return new SwisstronikSigningStargateClient(undefined, signer, { + registry: options?.registry ? options.registry : createDefaultIdentityRegistry(), + ...options + }) + } + constructor( tmClient: Tendermint37Client | Tendermint34Client | undefined, signer: OfflineSigner, @@ -137,8 +150,9 @@ export class SwisstronikSigningStargateClient extends SigningStargateClient { this._signer = signer if (options.gasPrice) this._gasPrice = options.gasPrice - const { accountParser = this.accountFromAny } = options; + const { accountParser = this.accountFromAny, aminoTypes = new AminoTypes(createDefaultAminoConverters()), } = options; this.overridenAccountParser = accountParser; + this._aminoTypes = aminoTypes; } public async signAndBroadcast( @@ -183,8 +197,7 @@ export class SwisstronikSigningStargateClient extends SigningStargateClient { chainId: chainId, } } - - return this._signDirect(signerAddress, messages, fee, memo, signerData) + return isOfflineDirectSigner(this._signer)? this._signDirect(signerAddress, messages, fee, memo, signerData) : this._signAmino(signerAddress, messages, fee, memo, signerData); } public async getAccount(searchAddress: string): Promise { @@ -255,6 +268,50 @@ export class SwisstronikSigningStargateClient extends SigningStargateClient { }) } + private async _signAmino( + signerAddress: string, + messages: readonly EncodeObject[], + fee: DidStdFee, + memo: string, + { accountNumber, sequence, chainId }: SignerData, + ) { + assert(!isOfflineDirectSigner(this._signer)) + + const accountFromSigner = (await this._signer.getAccounts()).find((account) => account.address === signerAddress); + if (!accountFromSigner) { + throw new Error("Failed to retrieve account from signer"); + } + + const pubkey = Any.fromPartial({ + typeUrl: "/ethermint.crypto.v1.ethsecp256k1.PubKey", + value: CommonPubKey.encode({ + key: accountFromSigner.pubkey + }).finish(), + }) + + const signMode = SignMode.SIGN_MODE_LEGACY_AMINO_JSON; + const msgs = messages.map((msg) => this._aminoTypes.toAmino(msg)); + const signDoc = makeSignDocAmino(msgs, fee, chainId, memo, accountNumber, sequence); + const { signature, signed } = await this._signer.signAmino(signerAddress, signDoc); + const signedTxBody = { + messages: signed.msgs.map((msg) => this._aminoTypes.fromAmino(msg)), + memo: signed.memo, + }; + const signedTxBodyEncodeObject = { + typeUrl: "/cosmos.tx.v1beta1.TxBody", + value: signedTxBody, + }; + const signedTxBodyBytes = this.registry.encode(signedTxBodyEncodeObject); + const signedGasLimit = Int53.fromString(signed.fee.gas).toNumber(); + const signedSequence = Int53.fromString(signed.sequence).toNumber(); + const signedAuthInfoBytes = makeAuthInfoBytes([{ pubkey, sequence: signedSequence }], signed.fee.amount, signedGasLimit, signed.fee.granter, signed.fee.payer, signMode); + return TxRaw.fromPartial({ + bodyBytes: signedTxBodyBytes, + authInfoBytes: signedAuthInfoBytes, + signatures: [fromBase64(signature.signature)], + }); + } + async checkDidSigners(verificationMethods: Partial[] = []): Promise { if (verificationMethods.length === 0) { throw new Error('No verification methods provided') @@ -384,11 +441,26 @@ export class SwisstronikSigningStargateClient extends SigningStargateClient { case "/cosmos.crypto.ed25519.PubKey": { return this.anyToSinglePubkey(pubkey); } + case "/cosmos.crypto.multisig.LegacyAminoPubKey": { + return this.anyToMultiPubkey(pubkey); + } default: throw new Error(`Pubkey type_url ${pubkey.typeUrl} not recognized`); } } + private anyToMultiPubkey(pubkey: Any): MultisigThresholdPubkey { + const { publicKeys, threshold } = LegacyAminoPubKey.decode(pubkey.value); + const keys = publicKeys.map((key) => this.anyToSinglePubkey(key)); + return { + type: "tendermint/PubKeyMultisigThreshold", + value: { + pubkeys: keys, + threshold: String(threshold) + }, + }; + } + private anyToSinglePubkey(pubkey: Any): SinglePubkey { switch (pubkey.typeUrl) { case "/ethermint.crypto.v1.ethsecp256k1.PubKey":