Skip to content

Commit

Permalink
feat(did): Added automated did creation tests & fixes, refactors
Browse files Browse the repository at this point in the history
  • Loading branch information
Eengineer1 committed Jul 21, 2022
1 parent 59a88eb commit bcfc198
Show file tree
Hide file tree
Showing 8 changed files with 594 additions and 62 deletions.
322 changes: 294 additions & 28 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@
},
"dependencies": {
"@cheqd/ts-proto": "^1.0.1",
"@cosmjs/amino": "^0.28.11",
"@cosmjs/encoding": "^0.28.11",
"@cosmjs/math": "^0.28.11",
"@cosmjs/proto-signing": "^0.28.10",
"@cosmjs/stargate": "^0.28.10",
"@cosmjs/tendermint-rpc": "^0.28.10",
"@cosmjs/utils": "^0.28.11",
"@stablelib/ed25519": "^1.0.2",
"cosmjs-types": "^0.5.0",
"did-jwt": "^6.2.0",
Expand Down
33 changes: 29 additions & 4 deletions src/modules/did.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { createProtobufRpcClient, DeliverTxResponse, QueryClient } from "@cosmjs/stargate"
import { createProtobufRpcClient, DeliverTxResponse, QueryClient, StdFee } from "@cosmjs/stargate"
/* import { QueryClientImpl } from '@cheqd/ts-proto/cheqd/v1/query' */
import { CheqdExtension, AbstractCheqdSDKModule, MinimalImportableCheqdSDKModule } from "./_"
import { CheqdSigningStargateClient } from "../signer"
import { IContext } from "../types"
import { DidStdFee, IContext, ISignInputs } from "../types"
import { MsgCreateDid, MsgCreateDidPayload } from "@cheqd/ts-proto/cheqd/v1/tx"
import { MsgCreateDidEncodeObject, typeUrlMsgCreateDid } from "../registry"
import { VerificationMethod } from "@cheqd/ts-proto/cheqd/v1/did"

export class DIDModule extends AbstractCheqdSDKModule {
constructor(signer: CheqdSigningStargateClient){
Expand All @@ -13,11 +16,33 @@ export class DIDModule extends AbstractCheqdSDKModule {
}
}

async createDidTx(did: string, publicKey: string, context?: IContext): Promise<string> {
async createDidTx(signInputs: ISignInputs[], didPayload: Partial<MsgCreateDidPayload>, address: string, fee: DidStdFee | 'auto' | number, memo?: string, context?: IContext): Promise<DeliverTxResponse> {
if (!this._signer) {
this._signer = context!.sdk!.signer
}
return ''

const payload = MsgCreateDidPayload.fromPartial(didPayload)
const signatures = await this._signer.signDidTx(signInputs, payload)

console.warn(payload)
console.warn(signatures)

const value: MsgCreateDid = {
payload,
signatures
}

const createDidMsg: MsgCreateDidEncodeObject = {
typeUrl: typeUrlMsgCreateDid,
value
}

return this._signer.signAndBroadcast(
address,
[createDidMsg],
fee,
memo
)
}

async updateDidTx(did: string, publicKey: string): Promise<string> {
Expand Down
160 changes: 148 additions & 12 deletions src/signer.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,73 @@
import { CheqdExtensions, setupCheqdExtensions } from './modules/_'
import { EncodeObject, OfflineSigner } from "@cosmjs/proto-signing";
import { DeliverTxResponse, HttpEndpoint, QueryClient, SigningStargateClient, SigningStargateClientOptions, StdFee } from "@cosmjs/stargate";
import { Tendermint34Client } from "@cosmjs/tendermint-rpc";
import { createDefaultCheqdRegistry } from "./registry";
import { MsgCreateDidPayload, SignInfo } from '@cheqd/ts-proto/cheqd/v1/tx';
import { ISignInputs, TSignerAlgo, VerificationMethods } from './types';
import { VerificationMethod } from '@cheqd/ts-proto/cheqd/v1/did';
import { EdDSASigner, hexToBytes, Signer } from 'did-jwt';
import { CheqdExtensions } from './modules/_'
import { EncodeObject, isOfflineDirectSigner, OfflineSigner, encodePubkey, TxBodyEncodeObject, makeAuthInfoBytes, makeSignDoc } from "@cosmjs/proto-signing"
import { DeliverTxResponse, GasPrice, HttpEndpoint, QueryClient, SigningStargateClient, SigningStargateClientOptions, StdFee, calculateFee, SignerData } from "@cosmjs/stargate"
import { Tendermint34Client } from "@cosmjs/tendermint-rpc"
import { createDefaultCheqdRegistry } from "./registry"
import { MsgCreateDidPayload, SignInfo } from '@cheqd/ts-proto/cheqd/v1/tx'
import { DidStdFee, ISignInputs, TSignerAlgo, VerificationMethods } from './types'
import { VerificationMethod } from '@cheqd/ts-proto/cheqd/v1/did'
import { base64ToBytes, EdDSASigner, hexToBytes, Signer } from 'did-jwt'
import { toString } from 'uint8arrays'
import { assert, assertDefined } from '@cosmjs/utils'
import { encodeSecp256k1Pubkey } from '@cosmjs/amino'
import { Int53 } from '@cosmjs/math'
import { fromBase64 } from '@cosmjs/encoding'
import { AuthInfo, SignerInfo, TxRaw } from 'cosmjs-types/cosmos/tx/v1beta1/tx'
import { SignMode } from 'cosmjs-types/cosmos/tx/signing/v1beta1/signing'
import { Any } from 'cosmjs-types/google/protobuf/any'
import { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin'
import Long from 'long'

export function calculateDidFee(gasLimit: number, gasPrice: string | GasPrice): DidStdFee {
return calculateFee(gasLimit, gasPrice)
}

export function makeSignerInfos(
signers: ReadonlyArray<{ readonly pubkey: Any; readonly sequence: number }>,
signMode: SignMode,
): SignerInfo[] {
return signers.map(
({ pubkey, sequence }): SignerInfo => ({
publicKey: pubkey,
modeInfo: {
single: { mode: signMode },
},
sequence: Long.fromNumber(sequence),
}),
);
}

export function makeDidAuthInfoBytes(
signers: ReadonlyArray<{ readonly pubkey: Any; readonly sequence: number }>,
feeAmount: readonly Coin[],
gasLimit: number,
feePayer: string,
signMode = SignMode.SIGN_MODE_DIRECT,
): Uint8Array {
const authInfo = {
signerInfos: makeSignerInfos(signers, signMode),
fee: {
amount: [...feeAmount],
gasLimit: Long.fromNumber(gasLimit),
payer: feePayer
}
}
return AuthInfo.encode(AuthInfo.fromPartial(authInfo)).finish()
}


export class CheqdSigningStargateClient extends SigningStargateClient {
public readonly cheqdExtensions: CheqdExtensions | undefined
private didSigners: TSignerAlgo = {}
private readonly _gasPrice: GasPrice | undefined
private readonly _signer: OfflineSigner

public static async connectWithSigner(endpoint: string | HttpEndpoint, signer: OfflineSigner, options?: SigningStargateClientOptions | undefined): Promise<CheqdSigningStargateClient> {
const tmClient = await Tendermint34Client.connect(endpoint)
return new CheqdSigningStargateClient(tmClient, signer, {
registry: createDefaultCheqdRegistry(),
...options
});
})
}

constructor(
Expand All @@ -27,6 +76,8 @@ export class CheqdSigningStargateClient extends SigningStargateClient {
options: SigningStargateClientOptions = {}
) {
super(tmClient, signer, options)
this._signer = signer
if (options.gasPrice) this._gasPrice = options.gasPrice

/** GRPC Connection */

Expand Down Expand Up @@ -60,14 +111,99 @@ export class CheqdSigningStargateClient extends SigningStargateClient {
return this.didSigners[verificationMethod] ?? EdDSASigner
}

async signDIDTx(signInputs: ISignInputs[], payload: MsgCreateDidPayload): Promise<SignInfo[]> {
async signAndBroadcast(
signerAddress: string,
messages: readonly EncodeObject[],
fee: DidStdFee | "auto" | number,
memo = "",
): Promise<DeliverTxResponse> {
let usedFee: DidStdFee
if (fee == "auto" || typeof fee === "number") {
assertDefined(this._gasPrice, "Gas price must be set in the client options when auto gas is used.")
const gasEstimation = await this.simulate(signerAddress, messages, memo)
const multiplier = typeof fee === "number" ? fee : 1.3
usedFee = calculateDidFee(Math.round(gasEstimation * multiplier), this._gasPrice)
usedFee.payer = signerAddress
} else {
usedFee = fee
assertDefined(usedFee.payer, "Payer address must be set when fee is not auto.")
signerAddress = usedFee.payer!
}
const txRaw = await this.sign(signerAddress, messages, usedFee, memo)
const txBytes = TxRaw.encode(txRaw).finish()
return this.broadcastTx(txBytes, this.broadcastTimeoutMs, this.broadcastPollIntervalMs)
}

public async sign(
signerAddress: string,
messages: readonly EncodeObject[],
fee: DidStdFee,
memo: string,
explicitSignerData?: SignerData,
): Promise<TxRaw> {
let signerData: SignerData
if (explicitSignerData) {
signerData = explicitSignerData
} else {
const { accountNumber, sequence } = await this.getSequence(signerAddress)
const chainId = await this.getChainId()
signerData = {
accountNumber: accountNumber,
sequence: sequence,
chainId: chainId,
}
}

return this._signDirect(signerAddress, messages, fee, memo, signerData)

// TODO: override signAmino as well
/* return isOfflineDirectSigner(this._signer)
? this._signDirect(signerAddress, messages, fee, memo, signerData)
: this._signAmino(signerAddress, messages, fee, memo, signerData) */
}

private async _signDirect(
signerAddress: string,
messages: readonly EncodeObject[],
fee: DidStdFee,
memo: string,
{ accountNumber, sequence, chainId }: SignerData,
): Promise<TxRaw> {
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 = encodePubkey(encodeSecp256k1Pubkey(accountFromSigner.pubkey))
const txBodyEncodeObject: TxBodyEncodeObject = {
typeUrl: "/cosmos.tx.v1beta1.TxBody",
value: {
messages: messages,
memo: memo,
},
}
const txBodyBytes = this.registry.encode(txBodyEncodeObject)
const gasLimit = Int53.fromString(fee.gas).toNumber()
const authInfoBytes = makeDidAuthInfoBytes([{ pubkey, sequence }], fee.amount, gasLimit, fee.payer!)
const signDoc = makeSignDoc(txBodyBytes, authInfoBytes, chainId, accountNumber)
const { signature, signed } = await this._signer.signDirect(signerAddress, signDoc)
return TxRaw.fromPartial({
bodyBytes: signed.bodyBytes,
authInfoBytes: signed.authInfoBytes,
signatures: [fromBase64(signature.signature)],
})
}

async signDidTx(signInputs: ISignInputs[], payload: MsgCreateDidPayload): Promise<SignInfo[]> {
await this.checkDidSigners(payload?.verificationMethod)

const signBytes = MsgCreateDidPayload.encode(payload).finish()
const signInfos: SignInfo[] = await Promise.all(signInputs.map(async (signInput) => {
return {
verificationMethodId: signInput.verificationMethodId,
signature: await (await this.getDidSigner(signInput.verificationMethodId, payload.verificationMethod))(hexToBytes(signInput.privateKeyHex))(signBytes) as string
signature: toString(base64ToBytes((await (await this.getDidSigner(signInput.verificationMethodId, payload.verificationMethod))(hexToBytes(signInput.privateKeyHex))(signBytes)) as string), 'base64pad')
}
}))

Expand Down
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CheqdSDK } from "."
import { EdDSASigner, Signer } from 'did-jwt'
import { Coin } from "@cosmjs/proto-signing"

export enum CheqdNetwork {
Mainnet = 'mainnet',
Expand Down Expand Up @@ -38,4 +39,11 @@ export interface IKeyPair {
export interface IKeyValuePair {
key: string
value: any
}

export interface DidStdFee {
readonly amount: readonly Coin[]
readonly gas: string
payer?: string
granter?: string
}
57 changes: 57 additions & 0 deletions tests/modules/did.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"
import { DeliverTxResponse } from "@cosmjs/stargate"
import { fromString, toString } from 'uint8arrays'
import { DIDModule } from "../../src"
import { CheqdSigningStargateClient } from "../../src/signer"
import { CheqdNetwork, DidStdFee, ISignInputs, VerificationMethods } from "../../src/types"
import { createDidPayload, createKeyPairBase64, exampleCheqdNetwork, faucet } from "../testutils.test"


describe('DIDModule', () => {
describe('constructor', () => {
it('should instantiate standalone module', async () => {
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic)
const signer = await CheqdSigningStargateClient.connectWithSigner(exampleCheqdNetwork.rpcUrl, wallet)
const didModule = new DIDModule(signer)
expect(didModule).toBeInstanceOf(DIDModule)
})
})

describe('createDidTx', () => {
it('should create a new DID', async () => {
jest.setTimeout(10000)

const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic, {prefix: faucet.prefix})
const signer = await CheqdSigningStargateClient.connectWithSigner(exampleCheqdNetwork.rpcUrl, wallet)
const didModule = new DIDModule(signer)
const keyPair = createKeyPairBase64()
const didPayload = createDidPayload(keyPair, VerificationMethods.JWK, CheqdNetwork.Testnet)
const signInputs: ISignInputs[] = [
{
verificationMethodId: didPayload.verificationMethod[0].id,
privateKeyHex: toString(fromString(keyPair.privateKey, 'base64'), 'hex')
}
]
const fee: DidStdFee = {
amount: [
{
denom: 'ncheq',
amount: '5000000'
}
],
gas: '100000',
payer: (await wallet.getAccounts())[0].address
}
const didTx: DeliverTxResponse = await didModule.createDidTx(
signInputs,
didPayload,
(await wallet.getAccounts())[0].address,
fee
)

console.warn(didTx)

expect(didTx.code).toBe(0)
})
})
})

0 comments on commit bcfc198

Please sign in to comment.