Skip to content

Commit 40bb485

Browse files
authored
feat: Resources support [DEV-1616] (#18)
1 parent 3448e9e commit 40bb485

File tree

9 files changed

+263
-28
lines changed

9 files changed

+263
-28
lines changed

.github/workflows/build.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,5 @@ jobs:
2727
- name: "Run npm build"
2828
run: npm run build
2929

30-
# - name: "Run npm test"
31-
# run: npm run test
30+
- name: "Run npm test"
31+
run: npm run test

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
"main": "build/index.js",
99
"types": "build/index.d.ts",
1010
"scripts": {
11-
"test": "jest --passWithNoTests",
12-
"test:watch": "jest --passWithNoTests --watch",
11+
"test": "jest --passWithNoTests --runInBand",
12+
"test:watch": "jest --passWithNoTests --runInBand --watch",
1313
"build": "tsc"
1414
},
1515
"repository": "https://github.com/cheqd/sdk.git",

src/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { OfflineSigner, Registry } from '@cosmjs/proto-signing'
22
import { DIDModule, MinimalImportableDIDModule } from './modules/did'
3-
import { MinimalImportableResourcesModule, ResourcesModule } from './modules/resources'
3+
import { MinimalImportableResourcesModule, ResourceModule } from './modules/resource'
44
import { AbstractCheqdSDKModule, applyMixins, instantiateCheqdSDKModule, instantiateCheqdSDKModuleRegistryTypes, } from './modules/_'
55
import { createDefaultCheqdRegistry } from './registry'
66
import { CheqdSigningStargateClient } from './signer'
@@ -101,7 +101,7 @@ export async function createCheqdSDK(options: ICheqdSDKOptions): Promise<CheqdSD
101101
return await (new CheqdSDK(options)).build()
102102
}
103103

104-
export { DIDModule, ResourcesModule }
104+
export { DIDModule, ResourceModule }
105105
export { createSignInputsFromImportableEd25519Key }
106106
export {
107107
createKeyPairRaw,

src/modules/resource.ts

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { AbstractCheqdSDKModule, MinimalImportableCheqdSDKModule } from "./_"
2+
import { CheqdSigningStargateClient } from "../signer"
3+
import { EncodeObject, GeneratedType } from "@cosmjs/proto-signing"
4+
import { DidStdFee, IContext, ISignInputs } from '../types';
5+
import { MsgCreateResource, MsgCreateResourcePayload, MsgCreateResourceResponse, protobufPackage } from "@cheqd/ts-proto/resource/v1/tx"
6+
import { DeliverTxResponse } from "@cosmjs/stargate"
7+
import { Writer } from "protobufjs"
8+
9+
export const typeUrlMsgCreateResource = `/${protobufPackage}.MsgCreateResource`
10+
export const typeUrlMsgCreateResourceResponse = `/${protobufPackage}.MsgCreateResourceResponse`
11+
12+
export interface MsgCreateResourceEncodeObject extends EncodeObject {
13+
readonly typeUrl: typeof typeUrlMsgCreateResource,
14+
readonly value: Partial<MsgCreateResource>
15+
}
16+
17+
export function isMsgCreateResourceEncodeObject(obj: EncodeObject): obj is MsgCreateResourceEncodeObject {
18+
return obj.typeUrl === typeUrlMsgCreateResource
19+
}
20+
21+
export class ResourceModule extends AbstractCheqdSDKModule {
22+
static readonly registryTypes: Iterable<[string, GeneratedType]> = [
23+
[typeUrlMsgCreateResource, MsgCreateResource],
24+
[typeUrlMsgCreateResourceResponse, MsgCreateResourceResponse]
25+
]
26+
27+
constructor(signer: CheqdSigningStargateClient) {
28+
super(signer)
29+
this.methods = {
30+
createResourceTx: this.createResourceTx.bind(this)
31+
}
32+
}
33+
34+
public getRegistryTypes(): Iterable<[string, GeneratedType]> {
35+
return []
36+
}
37+
38+
// We need this workagound because amino encoding is used in cheqd-node to derive sign bytes for identity messages.
39+
// In most cases it works the same way as protobuf encoding, but in the MsgCreateResourcePayload
40+
// we use non-default property indexes so we need this separate encoding function.
41+
// TODO: Remove this workaround when cheqd-node will use protobuf encoding.
42+
static getMsgCreateResourcePayloadAminoSignBytes(message: MsgCreateResourcePayload): Uint8Array {
43+
const writer = new Writer();
44+
45+
if (message.collectionId !== "") {
46+
writer.uint32(10).string(message.collectionId);
47+
}
48+
if (message.id !== "") {
49+
writer.uint32(18).string(message.id);
50+
}
51+
if (message.name !== "") {
52+
writer.uint32(26).string(message.name);
53+
}
54+
if (message.resourceType !== "") {
55+
writer.uint32(34).string(message.resourceType);
56+
}
57+
if (message.data.length !== 0) {
58+
// Animo coded assigns index 5 to this property. In proto definitions it's 6.
59+
// Since we use amino on node + non default property indexing, we need to encode it manually.
60+
writer.uint32(42).bytes(message.data);
61+
}
62+
63+
return writer.finish();
64+
}
65+
66+
static async signPayload(payload: MsgCreateResourcePayload, signInputs: ISignInputs[]): Promise<MsgCreateResource> {
67+
const signBytes = ResourceModule.getMsgCreateResourcePayloadAminoSignBytes(payload)
68+
const signatures = await CheqdSigningStargateClient.signIdentityTx(signBytes, signInputs)
69+
70+
return {
71+
payload,
72+
signatures
73+
}
74+
}
75+
76+
async createResourceTx(signInputs: ISignInputs[], resourcePayload: Partial<MsgCreateResourcePayload>, address: string, fee: DidStdFee | 'auto' | number, memo?: string, context?: IContext): Promise<DeliverTxResponse> {
77+
if (!this._signer) {
78+
this._signer = context!.sdk!.signer
79+
}
80+
81+
const payload = MsgCreateResourcePayload.fromPartial(resourcePayload)
82+
83+
const msg = await ResourceModule.signPayload(payload, signInputs)
84+
85+
const encObj: MsgCreateResourceEncodeObject = {
86+
typeUrl: typeUrlMsgCreateResource,
87+
value: msg
88+
}
89+
90+
return this._signer.signAndBroadcast(
91+
address,
92+
[encObj],
93+
fee,
94+
memo
95+
)
96+
}
97+
}
98+
99+
export type MinimalImportableResourcesModule = MinimalImportableCheqdSDKModule<ResourceModule>

src/modules/resources.ts

-18
This file was deleted.

src/signer.ts

+35-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { DeliverTxResponse, GasPrice, HttpEndpoint, QueryClient, SigningStargate
44
import { Tendermint34Client } from "@cosmjs/tendermint-rpc"
55
import { createDefaultCheqdRegistry } from "./registry"
66
import { MsgCreateDidPayload, SignInfo, MsgUpdateDidPayload } from '@cheqd/ts-proto/cheqd/v1/tx';
7-
import { DidStdFee, ISignInputs, TSignerAlgo, VerificationMethods } from './types'
7+
import { DidStdFee, ISignInputs, TSignerAlgo, VerificationMethods } from './types';
88
import { VerificationMethod } from '@cheqd/ts-proto/cheqd/v1/did'
9-
import { base64ToBytes, EdDSASigner, hexToBytes, Signer } from 'did-jwt'
9+
import { base64ToBytes, EdDSASigner, hexToBytes, Signer, ES256Signer, ES256KSigner } from 'did-jwt';
1010
import { toString } from 'uint8arrays'
1111
import { assert, assertDefined } from '@cosmjs/utils'
1212
import { encodeSecp256k1Pubkey } from '@cosmjs/amino'
@@ -227,4 +227,37 @@ export class CheqdSigningStargateClient extends SigningStargateClient {
227227

228228
return signInfos
229229
}
230+
231+
static async signIdentityTx(signBytes: Uint8Array, signInputs: ISignInputs[]): Promise<SignInfo[]> {
232+
let signInfos: SignInfo[] = [];
233+
234+
for (let signInput of signInputs) {
235+
if (typeof(signInput.keyType) === undefined) {
236+
throw new Error('Key type is not defined')
237+
}
238+
239+
let signature: string;
240+
241+
switch (signInput.keyType) {
242+
case 'Ed25519':
243+
signature = (await EdDSASigner(hexToBytes(signInput.privateKeyHex))(signBytes)) as string;
244+
break;
245+
case 'Secp256k1':
246+
signature = (await ES256KSigner(hexToBytes(signInput.privateKeyHex))(signBytes)) as string;
247+
break;
248+
case 'P256':
249+
signature = (await ES256Signer(hexToBytes(signInput.privateKeyHex))(signBytes)) as string;
250+
break;
251+
default:
252+
throw new Error(`Unsupported signature type: ${signInput.keyType}`);
253+
}
254+
255+
signInfos.push({
256+
verificationMethodId: signInput.verificationMethodId,
257+
signature: toString(base64ToBytes(signature), 'base64pad')
258+
});
259+
}
260+
261+
return signInfos
262+
}
230263
}

src/types.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { CheqdSDK } from "."
2-
import { EdDSASigner, Signer } from 'did-jwt'
32
import { Coin } from "@cosmjs/proto-signing"
3+
import { Signer } from "did-jwt"
44

55
export enum CheqdNetwork {
66
Mainnet = 'mainnet',
@@ -33,6 +33,7 @@ export type TSignerAlgo = {
3333

3434
export interface ISignInputs {
3535
verificationMethodId: string
36+
keyType?: 'Ed25519' | 'Secp256k1' | 'P256'
3637
privateKeyHex: string
3738
}
3839

tests/index.test.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { DirectSecp256k1HdWallet, GeneratedType } from '@cosmjs/proto-signing'
2-
import { createCheqdSDK, DIDModule, ICheqdSDKOptions } from '../src/index'
2+
import { createCheqdSDK, DIDModule, ICheqdSDKOptions, ResourceModule } from '../src/index'
33
import { exampleCheqdNetwork, faucet } from './testutils.test'
44
import { AbstractCheqdSDKModule } from '../src/modules/_'
55
import { CheqdSigningStargateClient } from '../src/signer'
@@ -76,6 +76,21 @@ describe(
7676

7777
expect(cheqdSDK.signer.registry).toStrictEqual(cheqdRegistry)
7878
})
79+
80+
it('should instantiate registry from multiple passed modules', async () => {
81+
const options = {
82+
modules: [DIDModule as unknown as AbstractCheqdSDKModule, ResourceModule as unknown as AbstractCheqdSDKModule],
83+
rpcUrl: exampleCheqdNetwork.rpcUrl,
84+
wallet: await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic)
85+
} as ICheqdSDKOptions
86+
const cheqdSDK = await createCheqdSDK(options)
87+
88+
const didRegistryTypes = DIDModule.registryTypes
89+
const resourceRegistryTypes = ResourceModule.registryTypes
90+
const cheqdRegistry = createDefaultCheqdRegistry([...didRegistryTypes, ...resourceRegistryTypes])
91+
92+
expect(cheqdSDK.signer.registry).toStrictEqual(cheqdRegistry)
93+
})
7994
})
8095
}
8196
)

tests/modules/resource.test.ts

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { DirectSecp256k1HdWallet, GeneratedType } from "@cosmjs/proto-signing"
2+
import { DeliverTxResponse } from "@cosmjs/stargate"
3+
import { sign } from "@stablelib/ed25519"
4+
import { fromString, toString } from 'uint8arrays'
5+
import { DIDModule, ResourceModule } from "../../src"
6+
import { createDefaultCheqdRegistry } from "../../src/registry"
7+
import { CheqdSigningStargateClient } from "../../src/signer"
8+
import { DidStdFee, ISignInputs, MethodSpecificIdAlgo, VerificationMethods } from '../../src/types';
9+
import { createDidPayload, createDidVerificationMethod, createKeyPairBase64, createVerificationKeys, exampleCheqdNetwork, faucet } from "../testutils.test"
10+
import { MsgCreateResourcePayload } from '@cheqd/ts-proto/resource/v1/tx';
11+
import { randomUUID } from "crypto"
12+
13+
const defaultAsyncTxTimeout = 20000
14+
15+
describe('ResourceModule', () => {
16+
describe('constructor', () => {
17+
it('should instantiate standalone module', async () => {
18+
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic)
19+
const signer = await CheqdSigningStargateClient.connectWithSigner(exampleCheqdNetwork.rpcUrl, wallet)
20+
const resourceModule = new ResourceModule(signer)
21+
expect(resourceModule).toBeInstanceOf(ResourceModule)
22+
})
23+
})
24+
25+
describe('createResourceTx', () => {
26+
it('should create a new Resource', async () => {
27+
// Creating a DID
28+
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic, {prefix: faucet.prefix})
29+
30+
const registry = createDefaultCheqdRegistry(Array.from(DIDModule.registryTypes).concat(Array.from(ResourceModule.registryTypes)))
31+
32+
const signer = await CheqdSigningStargateClient.connectWithSigner(exampleCheqdNetwork.rpcUrl, wallet, { registry })
33+
34+
const didModule = new DIDModule(signer)
35+
36+
const keyPair = createKeyPairBase64()
37+
const verificationKeys = createVerificationKeys(keyPair, MethodSpecificIdAlgo.Base58, 'key-1', 16)
38+
const verificationMethods = createDidVerificationMethod([VerificationMethods.Base58], [verificationKeys])
39+
const didPayload = createDidPayload(verificationMethods, [verificationKeys])
40+
41+
const signInputs: ISignInputs[] = [
42+
{
43+
verificationMethodId: didPayload.verificationMethod[0].id,
44+
privateKeyHex: toString(fromString(keyPair.privateKey, 'base64'), 'hex')
45+
}
46+
]
47+
48+
const fee: DidStdFee = {
49+
amount: [
50+
{
51+
denom: 'ncheq',
52+
amount: '50000000'
53+
}
54+
],
55+
gas: '1000000',
56+
payer: (await wallet.getAccounts())[0].address
57+
}
58+
59+
const didTx: DeliverTxResponse = await didModule.createDidTx(
60+
signInputs,
61+
didPayload,
62+
(await wallet.getAccounts())[0].address,
63+
fee
64+
)
65+
66+
console.warn(`Using payload: ${JSON.stringify(didPayload)}`)
67+
console.warn(`DID Tx: ${JSON.stringify(didTx)}`)
68+
69+
expect(didTx.code).toBe(0)
70+
71+
// Creating a resource
72+
73+
const resourceModule = new ResourceModule(signer)
74+
75+
const resourcePayload: MsgCreateResourcePayload = {
76+
collectionId: didPayload.id.split(":").reverse()[0],
77+
id: randomUUID(),
78+
name: 'Test Resource',
79+
resourceType: 'test-resource-type',
80+
data: new TextEncoder().encode("{ \"message\": \"hello world\"}")
81+
}
82+
83+
console.warn(`Using payload: ${JSON.stringify(resourcePayload)}`)
84+
85+
const resourceSignInputs: ISignInputs[] = [
86+
{
87+
verificationMethodId: didPayload.verificationMethod[0].id,
88+
keyType: 'Ed25519',
89+
privateKeyHex: toString(fromString(keyPair.privateKey, 'base64'), 'hex')
90+
}
91+
]
92+
93+
const resourceTx = await resourceModule.createResourceTx(
94+
resourceSignInputs,
95+
resourcePayload,
96+
(await wallet.getAccounts())[0].address,
97+
fee
98+
)
99+
100+
console.warn(`DID Tx: ${JSON.stringify(resourceTx)}`)
101+
102+
expect(resourceTx.code).toBe(0)
103+
}, defaultAsyncTxTimeout)
104+
})
105+
})

0 commit comments

Comments
 (0)