Skip to content

Commit

Permalink
feat: Resources support [DEV-1616] (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
askolesov committed Sep 1, 2022
1 parent 3448e9e commit 40bb485
Show file tree
Hide file tree
Showing 9 changed files with 263 additions and 28 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ jobs:
- name: "Run npm build"
run: npm run build

# - name: "Run npm test"
# run: npm run test
- name: "Run npm test"
run: npm run test
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
"main": "build/index.js",
"types": "build/index.d.ts",
"scripts": {
"test": "jest --passWithNoTests",
"test:watch": "jest --passWithNoTests --watch",
"test": "jest --passWithNoTests --runInBand",
"test:watch": "jest --passWithNoTests --runInBand --watch",
"build": "tsc"
},
"repository": "https://github.com/cheqd/sdk.git",
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { OfflineSigner, Registry } from '@cosmjs/proto-signing'
import { DIDModule, MinimalImportableDIDModule } from './modules/did'
import { MinimalImportableResourcesModule, ResourcesModule } from './modules/resources'
import { MinimalImportableResourcesModule, ResourceModule } from './modules/resource'
import { AbstractCheqdSDKModule, applyMixins, instantiateCheqdSDKModule, instantiateCheqdSDKModuleRegistryTypes, } from './modules/_'
import { createDefaultCheqdRegistry } from './registry'
import { CheqdSigningStargateClient } from './signer'
Expand Down Expand Up @@ -101,7 +101,7 @@ export async function createCheqdSDK(options: ICheqdSDKOptions): Promise<CheqdSD
return await (new CheqdSDK(options)).build()
}

export { DIDModule, ResourcesModule }
export { DIDModule, ResourceModule }
export { createSignInputsFromImportableEd25519Key }
export {
createKeyPairRaw,
Expand Down
99 changes: 99 additions & 0 deletions src/modules/resource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { AbstractCheqdSDKModule, MinimalImportableCheqdSDKModule } from "./_"
import { CheqdSigningStargateClient } from "../signer"
import { EncodeObject, GeneratedType } from "@cosmjs/proto-signing"
import { DidStdFee, IContext, ISignInputs } from '../types';
import { MsgCreateResource, MsgCreateResourcePayload, MsgCreateResourceResponse, protobufPackage } from "@cheqd/ts-proto/resource/v1/tx"
import { DeliverTxResponse } from "@cosmjs/stargate"
import { Writer } from "protobufjs"

export const typeUrlMsgCreateResource = `/${protobufPackage}.MsgCreateResource`
export const typeUrlMsgCreateResourceResponse = `/${protobufPackage}.MsgCreateResourceResponse`

export interface MsgCreateResourceEncodeObject extends EncodeObject {
readonly typeUrl: typeof typeUrlMsgCreateResource,
readonly value: Partial<MsgCreateResource>
}

export function isMsgCreateResourceEncodeObject(obj: EncodeObject): obj is MsgCreateResourceEncodeObject {
return obj.typeUrl === typeUrlMsgCreateResource
}

export class ResourceModule extends AbstractCheqdSDKModule {
static readonly registryTypes: Iterable<[string, GeneratedType]> = [
[typeUrlMsgCreateResource, MsgCreateResource],
[typeUrlMsgCreateResourceResponse, MsgCreateResourceResponse]
]

constructor(signer: CheqdSigningStargateClient) {
super(signer)
this.methods = {
createResourceTx: this.createResourceTx.bind(this)
}
}

public getRegistryTypes(): Iterable<[string, GeneratedType]> {
return []
}

// We need this workagound because amino encoding is used in cheqd-node to derive sign bytes for identity messages.
// In most cases it works the same way as protobuf encoding, but in the MsgCreateResourcePayload
// we use non-default property indexes so we need this separate encoding function.
// TODO: Remove this workaround when cheqd-node will use protobuf encoding.
static getMsgCreateResourcePayloadAminoSignBytes(message: MsgCreateResourcePayload): Uint8Array {
const writer = new Writer();

if (message.collectionId !== "") {
writer.uint32(10).string(message.collectionId);
}
if (message.id !== "") {
writer.uint32(18).string(message.id);
}
if (message.name !== "") {
writer.uint32(26).string(message.name);
}
if (message.resourceType !== "") {
writer.uint32(34).string(message.resourceType);
}
if (message.data.length !== 0) {
// Animo coded assigns index 5 to this property. In proto definitions it's 6.
// Since we use amino on node + non default property indexing, we need to encode it manually.
writer.uint32(42).bytes(message.data);
}

return writer.finish();
}

static async signPayload(payload: MsgCreateResourcePayload, signInputs: ISignInputs[]): Promise<MsgCreateResource> {
const signBytes = ResourceModule.getMsgCreateResourcePayloadAminoSignBytes(payload)
const signatures = await CheqdSigningStargateClient.signIdentityTx(signBytes, signInputs)

return {
payload,
signatures
}
}

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

const payload = MsgCreateResourcePayload.fromPartial(resourcePayload)

const msg = await ResourceModule.signPayload(payload, signInputs)

const encObj: MsgCreateResourceEncodeObject = {
typeUrl: typeUrlMsgCreateResource,
value: msg
}

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

export type MinimalImportableResourcesModule = MinimalImportableCheqdSDKModule<ResourceModule>
18 changes: 0 additions & 18 deletions src/modules/resources.ts

This file was deleted.

37 changes: 35 additions & 2 deletions src/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { DeliverTxResponse, GasPrice, HttpEndpoint, QueryClient, SigningStargate
import { Tendermint34Client } from "@cosmjs/tendermint-rpc"
import { createDefaultCheqdRegistry } from "./registry"
import { MsgCreateDidPayload, SignInfo, MsgUpdateDidPayload } from '@cheqd/ts-proto/cheqd/v1/tx';
import { DidStdFee, ISignInputs, TSignerAlgo, VerificationMethods } from './types'
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 { base64ToBytes, EdDSASigner, hexToBytes, Signer, ES256Signer, ES256KSigner } from 'did-jwt';
import { toString } from 'uint8arrays'
import { assert, assertDefined } from '@cosmjs/utils'
import { encodeSecp256k1Pubkey } from '@cosmjs/amino'
Expand Down Expand Up @@ -227,4 +227,37 @@ export class CheqdSigningStargateClient extends SigningStargateClient {

return signInfos
}

static async signIdentityTx(signBytes: Uint8Array, signInputs: ISignInputs[]): Promise<SignInfo[]> {
let signInfos: SignInfo[] = [];

for (let signInput of signInputs) {
if (typeof(signInput.keyType) === undefined) {
throw new Error('Key type is not defined')
}

let signature: string;

switch (signInput.keyType) {
case 'Ed25519':
signature = (await EdDSASigner(hexToBytes(signInput.privateKeyHex))(signBytes)) as string;
break;
case 'Secp256k1':
signature = (await ES256KSigner(hexToBytes(signInput.privateKeyHex))(signBytes)) as string;
break;
case 'P256':
signature = (await ES256Signer(hexToBytes(signInput.privateKeyHex))(signBytes)) as string;
break;
default:
throw new Error(`Unsupported signature type: ${signInput.keyType}`);
}

signInfos.push({
verificationMethodId: signInput.verificationMethodId,
signature: toString(base64ToBytes(signature), 'base64pad')
});
}

return signInfos
}
}
3 changes: 2 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CheqdSDK } from "."
import { EdDSASigner, Signer } from 'did-jwt'
import { Coin } from "@cosmjs/proto-signing"
import { Signer } from "did-jwt"

export enum CheqdNetwork {
Mainnet = 'mainnet',
Expand Down Expand Up @@ -33,6 +33,7 @@ export type TSignerAlgo = {

export interface ISignInputs {
verificationMethodId: string
keyType?: 'Ed25519' | 'Secp256k1' | 'P256'
privateKeyHex: string
}

Expand Down
17 changes: 16 additions & 1 deletion tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DirectSecp256k1HdWallet, GeneratedType } from '@cosmjs/proto-signing'
import { createCheqdSDK, DIDModule, ICheqdSDKOptions } from '../src/index'
import { createCheqdSDK, DIDModule, ICheqdSDKOptions, ResourceModule } from '../src/index'
import { exampleCheqdNetwork, faucet } from './testutils.test'
import { AbstractCheqdSDKModule } from '../src/modules/_'
import { CheqdSigningStargateClient } from '../src/signer'
Expand Down Expand Up @@ -76,6 +76,21 @@ describe(

expect(cheqdSDK.signer.registry).toStrictEqual(cheqdRegistry)
})

it('should instantiate registry from multiple passed modules', async () => {
const options = {
modules: [DIDModule as unknown as AbstractCheqdSDKModule, ResourceModule as unknown as AbstractCheqdSDKModule],
rpcUrl: exampleCheqdNetwork.rpcUrl,
wallet: await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic)
} as ICheqdSDKOptions
const cheqdSDK = await createCheqdSDK(options)

const didRegistryTypes = DIDModule.registryTypes
const resourceRegistryTypes = ResourceModule.registryTypes
const cheqdRegistry = createDefaultCheqdRegistry([...didRegistryTypes, ...resourceRegistryTypes])

expect(cheqdSDK.signer.registry).toStrictEqual(cheqdRegistry)
})
})
}
)
105 changes: 105 additions & 0 deletions tests/modules/resource.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { DirectSecp256k1HdWallet, GeneratedType } from "@cosmjs/proto-signing"
import { DeliverTxResponse } from "@cosmjs/stargate"
import { sign } from "@stablelib/ed25519"
import { fromString, toString } from 'uint8arrays'
import { DIDModule, ResourceModule } from "../../src"
import { createDefaultCheqdRegistry } from "../../src/registry"
import { CheqdSigningStargateClient } from "../../src/signer"
import { DidStdFee, ISignInputs, MethodSpecificIdAlgo, VerificationMethods } from '../../src/types';
import { createDidPayload, createDidVerificationMethod, createKeyPairBase64, createVerificationKeys, exampleCheqdNetwork, faucet } from "../testutils.test"
import { MsgCreateResourcePayload } from '@cheqd/ts-proto/resource/v1/tx';
import { randomUUID } from "crypto"

const defaultAsyncTxTimeout = 20000

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

describe('createResourceTx', () => {
it('should create a new Resource', async () => {
// Creating a DID
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic, {prefix: faucet.prefix})

const registry = createDefaultCheqdRegistry(Array.from(DIDModule.registryTypes).concat(Array.from(ResourceModule.registryTypes)))

const signer = await CheqdSigningStargateClient.connectWithSigner(exampleCheqdNetwork.rpcUrl, wallet, { registry })

const didModule = new DIDModule(signer)

const keyPair = createKeyPairBase64()
const verificationKeys = createVerificationKeys(keyPair, MethodSpecificIdAlgo.Base58, 'key-1', 16)
const verificationMethods = createDidVerificationMethod([VerificationMethods.Base58], [verificationKeys])
const didPayload = createDidPayload(verificationMethods, [verificationKeys])

const signInputs: ISignInputs[] = [
{
verificationMethodId: didPayload.verificationMethod[0].id,
privateKeyHex: toString(fromString(keyPair.privateKey, 'base64'), 'hex')
}
]

const fee: DidStdFee = {
amount: [
{
denom: 'ncheq',
amount: '50000000'
}
],
gas: '1000000',
payer: (await wallet.getAccounts())[0].address
}

const didTx: DeliverTxResponse = await didModule.createDidTx(
signInputs,
didPayload,
(await wallet.getAccounts())[0].address,
fee
)

console.warn(`Using payload: ${JSON.stringify(didPayload)}`)
console.warn(`DID Tx: ${JSON.stringify(didTx)}`)

expect(didTx.code).toBe(0)

// Creating a resource

const resourceModule = new ResourceModule(signer)

const resourcePayload: MsgCreateResourcePayload = {
collectionId: didPayload.id.split(":").reverse()[0],
id: randomUUID(),
name: 'Test Resource',
resourceType: 'test-resource-type',
data: new TextEncoder().encode("{ \"message\": \"hello world\"}")
}

console.warn(`Using payload: ${JSON.stringify(resourcePayload)}`)

const resourceSignInputs: ISignInputs[] = [
{
verificationMethodId: didPayload.verificationMethod[0].id,
keyType: 'Ed25519',
privateKeyHex: toString(fromString(keyPair.privateKey, 'base64'), 'hex')
}
]

const resourceTx = await resourceModule.createResourceTx(
resourceSignInputs,
resourcePayload,
(await wallet.getAccounts())[0].address,
fee
)

console.warn(`DID Tx: ${JSON.stringify(resourceTx)}`)

expect(resourceTx.code).toBe(0)
}, defaultAsyncTxTimeout)
})
})

0 comments on commit 40bb485

Please sign in to comment.