Skip to content

Commit

Permalink
feat: stargate: Implement DID and Token features (#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
Youngjoon Lee authored Jul 19, 2021
1 parent de582ec commit c36b9b8
Show file tree
Hide file tree
Showing 8 changed files with 353 additions and 10 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
"@cosmjs/stargate": "0.25.5",
"@cosmjs/tendermint-rpc": "0.25.5",
"@cosmjs/utils": "0.25.5",
"@types/bs58": "4.0.1",
"@types/secp256k1": "4.0.3",
"bs58": "4.0.1",
"secp256k1": "4.0.2",
"uuid": "8.3.0"
},
"devDependencies": {
Expand Down
20 changes: 20 additions & 0 deletions src/crypto/secp256k1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import ecc from "secp256k1";
import { randomBytes } from "crypto";

export class Secp256k1 {
static generatePrivateKey(): Uint8Array {
let privKey
do {
privKey = randomBytes(32)
} while (!ecc.privateKeyVerify(privKey));
return privKey;
}

static getPublicKeyCompressed(privKey: Uint8Array): Uint8Array {
return ecc.publicKeyCreate(privKey);
}

static sign(data32: Uint8Array, privKey: Uint8Array): Uint8Array {
return ecc.ecdsaSign(data32, privKey).signature;
}
}
40 changes: 40 additions & 0 deletions src/did/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { sha256 } from "@cosmjs/crypto";
import { DIDDocument, DataWithSeq } from "../proto/panacea/did/v2/did";
import Long from "long";
import { Secp256k1 } from "../crypto/secp256k1";

const bs58 = require('bs58');

export class DidUtil {
static getDid(pubKeyCompressed: Uint8Array): string {
return `did:panacea:${bs58.encode(sha256(pubKeyCompressed))}`;
}

static getPublicKeyBase58(pubKeyCompressed: Uint8Array): string {
return bs58.encode(pubKeyCompressed);
}

static signDidDocument(privKey: Uint8Array, didDocument: DIDDocument, sequence: Long = Long.fromInt(0)): Uint8Array {
const dataWithSeq: DataWithSeq = {
data: DIDDocument.encode(didDocument).finish(),
sequence: sequence,
};
return Secp256k1.sign(sha256(DataWithSeq.encode(dataWithSeq).finish()), privKey);
}

static signDid(privKey: Uint8Array, did: string, sequence: Long = Long.fromInt(0)): Uint8Array {
const didDocument: DIDDocument = {
contexts: undefined,
id: did,
controller: undefined,
verificationMethods: [],
authentications: [],
assertionMethods: [],
keyAgreements: [],
capabilityInvocations: [],
capabilityDelegations: [],
services: [],
}
return this.signDidDocument(privKey, didDocument, sequence);
}
}
7 changes: 7 additions & 0 deletions src/panacea-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,12 @@ describe("PanaceaClient", () => {
expect(topic).toBeNull();
});
});

describe("getDid", () => {
it("works for non-existent DID", async () => {
const didDocumentWithSeq = await client.getDid("did:panacea:7Prd74ry1Uct87nZqL3ny7aR7Cg46JamVbJgk8azVgUm");
expect(didDocumentWithSeq).toBeNull();
});
});
});
});
39 changes: 38 additions & 1 deletion src/panacea-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,19 @@ import {
QueryTopicsResponse,
QueryWritersResponse
} from "./proto/panacea/aol/v2/query";
import {
QueryClientImpl as DidQueryClientImpl,
} from "./proto/panacea/did/v2/query";
import {
QueryClientImpl as TokenQueryClientImpl, QueryTokensResponse,
} from "./proto/panacea/token/v2/query";
import { Topic } from "./proto/panacea/aol/v2/topic";
import { PageRequest } from "./proto/cosmos/base/query/v1beta1/pagination";
import { Writer } from "./proto/panacea/aol/v2/writer";
import { Record } from "./proto/panacea/aol/v2/record";
import Long from "long";
import { DIDDocumentWithSeq } from "./proto/panacea/did/v2/did";
import { Token } from "./proto/panacea/token/v2/token";

const rpcErrMsgNotFound = /rpc error: code = NotFound/i;

Expand Down Expand Up @@ -83,5 +91,34 @@ export class PanaceaClient extends StargateClient {
}
}

// TODO: implement x/did, x/token queries
async getDid(did: string): Promise<DIDDocumentWithSeq | null> {
const queryService = new DidQueryClientImpl(createProtobufRpcClient(this.forceGetQueryClient()));
try {
const resp = await queryService.DID({didBase64: new Buffer(did).toString('base64')});
return resp.didDocumentWithSeq ?? null;
} catch (error) {
if (rpcErrMsgNotFound.test(error)) {
return null;
}
throw error;
}
}

async getToken(symbol: string): Promise<Token | null> {
const queryService = new TokenQueryClientImpl(createProtobufRpcClient(this.forceGetQueryClient()));
try {
const resp = await queryService.Token({symbol: symbol});
return resp.token ?? null;
} catch (error) {
if (rpcErrMsgNotFound.test(error)) {
return null;
}
throw error;
}
}

async getTokens(pageRequest?: PageRequest): Promise<QueryTokensResponse> {
const queryService = new TokenQueryClientImpl(createProtobufRpcClient(this.forceGetQueryClient()));
return await queryService.Tokens({pagination: pageRequest});
}
}
105 changes: 105 additions & 0 deletions src/signing-panacea-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing';
import { v4 as uuidv4 } from "uuid";
import { TextEncoder } from "util";
import Long from "long";
import { DIDDocument } from "./proto/panacea/did/v2/did";
import { isBroadcastTxSuccess } from "@cosmjs/stargate";
import { Secp256k1 } from "./crypto/secp256k1";
import { DidUtil } from "./did/util";

describe("SigningPanaceaClient", () => {
pendingWithoutPanacead();
Expand Down Expand Up @@ -97,4 +101,105 @@ describe("SigningPanaceaClient", () => {
});
});
});

describe("DID", () => {
let fromAddress: string;
let client: SigningPanaceaClient;

beforeAll(async () => {
const [firstAccount] = await wallet.getAccounts();
fromAddress = firstAccount.address;
});

beforeEach(async () => {
client = await SigningPanaceaClient.connectWithSigner(panacead.tendermintUrl, wallet);
});

afterEach(() => {
client.disconnect();
});

it("createDid", async () => {
const privKey = Secp256k1.generatePrivateKey();
const didDocument = generateDidDocument(privKey);
const signature = DidUtil.signDidDocument(privKey, didDocument);

const res = await client.createDid(didDocument, didDocument.verificationMethods[0].id, signature, fromAddress);
expect(isBroadcastTxSuccess(res)).toBeTruthy();

const didDocumentWithSeq = await client.getPanaceaClient().getDid(didDocument.id);
expect(didDocumentWithSeq.document).toEqual(didDocument);
});

describe("mutateDid", () => {
let privKey: Uint8Array;
let didDocument: DIDDocument;

beforeEach(async () => {
privKey = Secp256k1.generatePrivateKey();
didDocument = generateDidDocument(privKey);
const signature = DidUtil.signDidDocument(privKey, didDocument);

const res = await client.createDid(didDocument, didDocument.verificationMethods[0].id, signature, fromAddress);
expect(isBroadcastTxSuccess(res)).toBeTruthy();
});

it("updateDid", async () => {
didDocument.assertionMethods.push({
verificationMethodId: didDocument.verificationMethods[0].id,
verificationMethod: undefined,
});
const signature = DidUtil.signDidDocument(privKey, didDocument);

const res = await client.updateDid(didDocument, didDocument.verificationMethods[0].id, signature, fromAddress);
expect(isBroadcastTxSuccess(res)).toBeTruthy();

const didDocumentWithSeq = await client.getPanaceaClient().getDid(didDocument.id);
expect(didDocumentWithSeq.document).toEqual(didDocument);
});

it("deactivateDid", async () => {
const signature = DidUtil.signDid(privKey, didDocument.id);

const res = await client.deactivateDid(didDocument.id, didDocument.verificationMethods[0].id, signature, fromAddress);
expect(isBroadcastTxSuccess(res)).toBeTruthy();

await expect(client.getPanaceaClient().getDid(didDocument.id)).rejects.toThrow(/DID was already deactivated/);
});
});
});
});

// A test utility function
function generateDidDocument(privKey: Uint8Array): DIDDocument {
const pubKeyCompressed = Secp256k1.getPublicKeyCompressed(privKey);

const did = DidUtil.getDid(pubKeyCompressed);
const verificationMethodId = `${did}#key1`;
return {
contexts: {
values: ['https://www.w3.org/ns/did/v1'],
},
id: did,
controller: undefined,
verificationMethods: [
{
id: verificationMethodId,
type: 'EcdsaSecp256k1VerificationKey2019',
controller: did,
publicKeyBase58: DidUtil.getPublicKeyBase58(pubKeyCompressed),
},
],
authentications: [
{
verificationMethodId: verificationMethodId,
verificationMethod: undefined,
}
],
assertionMethods: [],
keyAgreements: [],
capabilityInvocations: [],
capabilityDelegations: [],
services: [],
};
}
79 changes: 72 additions & 7 deletions src/signing-panacea-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import { SigningStargateClientOptions } from "@cosmjs/stargate/build/signingstar
import { PanaceaClient } from "./panacea-client";
import { stringToPath } from "@cosmjs/crypto";
import { MsgAddRecord, MsgAddWriter, MsgCreateTopic, MsgDeleteWriter } from "./proto/panacea/aol/v2/tx";
import { MsgCreateDID, MsgDeactivateDID, MsgUpdateDID } from "./proto/panacea/did/v2/tx";
import { MsgIssueToken } from "./proto/panacea/token/v2/tx";
import { DIDDocument } from "./proto/panacea/did/v2/did";
import { IntProto } from "./proto/cosmos/base/v1beta1/coin";

//////////////////////////////////////////////////////////////////////////////////////////////////
// TODO: This FeeTable concept is removed on the latest CosmJS (not released yet).
Expand Down Expand Up @@ -80,12 +84,20 @@ export class SigningPanaceaClient extends SigningStargateClient {
protected static msgTypeAddWriter = "/panacea.aol.v2.MsgAddWriter";
protected static msgTypeDeleteWriter = "/panacea.aol.v2.MsgDeleteWriter";
protected static msgTypeAddRecord = "/panacea.aol.v2.MsgAddRecord";
protected static msgTypeCreateDid = "/panacea.did.v2.MsgCreateDID";
protected static msgTypeUpdateDid = "/panacea.did.v2.MsgUpdateDID";
protected static msgTypeDeactivateDid = "/panacea.did.v2.MsgDeactivateDID";
protected static msgTypeIssueToken = "/panacea.token.v2.MsgIssueToken";

private static panaceaRegistryTypes: ReadonlyArray<[string, GeneratedType]> = [
[SigningPanaceaClient.msgTypeCreateTopic, MsgCreateTopic],
[SigningPanaceaClient.msgTypeAddWriter, MsgAddWriter],
[SigningPanaceaClient.msgTypeDeleteWriter, MsgDeleteWriter],
[SigningPanaceaClient.msgTypeAddRecord, MsgAddRecord],
[SigningPanaceaClient.msgTypeCreateDid, MsgCreateDID],
[SigningPanaceaClient.msgTypeUpdateDid, MsgUpdateDID],
[SigningPanaceaClient.msgTypeDeactivateDid, MsgDeactivateDID],
[SigningPanaceaClient.msgTypeIssueToken, MsgIssueToken],
];

constructor(tmClient: Tendermint34Client | undefined, signer: OfflineSigner, options: SigningPanaceaClientOptions) {
Expand Down Expand Up @@ -142,8 +154,8 @@ export class SigningPanaceaClient extends SigningStargateClient {
description: description,
writerAddress: writerAddress,
ownerAddress: ownerAddress,
}
}
},
};
return this.signAndBroadcast(ownerAddress, [msg], this.panaceaFees.addWriter, memo);
}

Expand All @@ -154,8 +166,8 @@ export class SigningPanaceaClient extends SigningStargateClient {
topicName: topicName,
writerAddress: writerAddress,
ownerAddress: ownerAddress,
}
}
},
};
return this.signAndBroadcast(ownerAddress, [msg], this.panaceaFees.deleteWriter, memo);
}

Expand All @@ -168,10 +180,63 @@ export class SigningPanaceaClient extends SigningStargateClient {
value: value,
writerAddress: writerAddress,
ownerAddress: ownerAddress,
}
}
},
};
return this.signAndBroadcast(writerAddress, [msg], this.panaceaFees.addWriter, memo);
}

// TODO: implement x/did, x/token transactions
async createDid(didDocument: DIDDocument, verficationMethodId: string, signature: Uint8Array, fromAddress: string, memo?: string): Promise<BroadcastTxResponse> {
const msg = {
typeUrl: SigningPanaceaClient.msgTypeCreateDid,
value: {
did: didDocument.id,
document: didDocument,
verificationMethodId: verficationMethodId,
signature: signature,
fromAddress: fromAddress,
},
};
return this.signAndBroadcast(fromAddress, [msg], this.panaceaFees.createDid, memo);
}

async updateDid(didDocument: DIDDocument, verficationMethodId: string, signature: Uint8Array, fromAddress: string, memo?: string): Promise<BroadcastTxResponse> {
const msg = {
typeUrl: SigningPanaceaClient.msgTypeUpdateDid,
value: {
did: didDocument.id,
document: didDocument,
verificationMethodId: verficationMethodId,
signature: signature,
fromAddress: fromAddress,
},
};
return this.signAndBroadcast(fromAddress, [msg], this.panaceaFees.updateDid, memo);
}

async deactivateDid(did: string, verficationMethodId: string, signature: Uint8Array, fromAddress: string, memo?: string): Promise<BroadcastTxResponse> {
const msg = {
typeUrl: SigningPanaceaClient.msgTypeDeactivateDid,
value: {
did: did,
verificationMethodId: verficationMethodId,
signature: signature,
fromAddress: fromAddress,
},
};
return this.signAndBroadcast(fromAddress, [msg], this.panaceaFees.deactivateDid, memo);
}

async issueToken(name: string, shortSymbol: string, totalSupplyMicro: IntProto, mintable: boolean, ownerAddress: string, memo?: string): Promise<BroadcastTxResponse> {
const msg = {
typeUrl: SigningPanaceaClient.msgTypeIssueToken,
value: {
name: name,
shortSymbol: shortSymbol,
totalSupplyMicro: totalSupplyMicro,
mintable: mintable,
ownerAddress: ownerAddress,
},
};
return this.signAndBroadcast(ownerAddress, [msg], this.panaceaFees.issueToken, memo);
}
}
Loading

0 comments on commit c36b9b8

Please sign in to comment.