diff --git a/package.json b/package.json index cb9130291..1a2050d3d 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,6 @@ "typedoc": "^0.24.8", "typescript": "^4.8.3" }, - "version": "1.0.0-alpha.1", + "version": "1.0.0-alpha.2", "packageManager": "yarn@4.1.1" } diff --git a/packages/asset-credentials/package.json b/packages/asset-credentials/package.json index a16a02319..aaf8f47a9 100644 --- a/packages/asset-credentials/package.json +++ b/packages/asset-credentials/package.json @@ -1,6 +1,6 @@ { "name": "@kiltprotocol/asset-credentials", - "version": "0.100.0-alpha.1", + "version": "0.100.0-alpha.2", "description": "", "main": "./lib/cjs/index.js", "module": "./lib/esm/index.js", diff --git a/packages/chain-helpers/package.json b/packages/chain-helpers/package.json index f8d82da43..56c4f1df0 100644 --- a/packages/chain-helpers/package.json +++ b/packages/chain-helpers/package.json @@ -1,6 +1,6 @@ { "name": "@kiltprotocol/chain-helpers", - "version": "0.100.0-alpha.1", + "version": "0.100.0-alpha.2", "description": "", "main": "./lib/cjs/index.js", "module": "./lib/esm/index.js", diff --git a/packages/chain-helpers/src/blockchain/Blockchain.ts b/packages/chain-helpers/src/blockchain/Blockchain.ts index 37812a189..ea7f053a0 100644 --- a/packages/chain-helpers/src/blockchain/Blockchain.ts +++ b/packages/chain-helpers/src/blockchain/Blockchain.ts @@ -5,25 +5,22 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { ApiPromise } from '@polkadot/api' +import { ApiPromise, SubmittableResult } from '@polkadot/api' import type { TxWithEvent } from '@polkadot/api-derive/types' import type { Vec } from '@polkadot/types' import type { Call, Extrinsic } from '@polkadot/types/interfaces' import type { AnyNumber, IMethod } from '@polkadot/types/types' import type { BN } from '@polkadot/util' + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- doing this instead of import '@kiltprotocol/augment-api' to avoid creating an import at runtime import type * as _ from '@kiltprotocol/augment-api' import type { ISubmittableResult, KeyringPair, - KiltAddress, - SignerInterface, SubmittableExtrinsic, SubscriptionPromise, + TransactionSigner, } from '@kiltprotocol/types' - -import { SubmittableResult } from '@polkadot/api' - import { ConfigService } from '@kiltprotocol/config' import { SDKErrors, Signers } from '@kiltprotocol/utils' @@ -170,10 +167,29 @@ export async function submitSignedTx( export const dispatchTx = submitSignedTx -export type TransactionSigner = SignerInterface< - 'Ecrecover-Secp256k1-Blake2b' | 'Sr25519' | 'Ed25519', - KiltAddress -> +/** + * Signs a SubmittableExtrinsic. + * + * @param tx An unsigned SubmittableExtrinsic. + * @param signer The {@link KeyringPair} used to sign the tx. + * @param opts Additional options. + * @param opts.tip Optional amount of Femto-KILT to tip the validator. + * @returns A signed {@link SubmittableExtrinsic}. + */ +export async function signTx( + tx: SubmittableExtrinsic, + signer: KeyringPair | TransactionSigner, + { tip }: { tip?: AnyNumber } = {} +): Promise { + if ('address' in signer) { + return tx.signAsync(signer, { tip }) + } + + return tx.signAsync(signer.id, { + tip, + signer: Signers.getPolkadotSigner([signer]), + }) +} /** * Signs and submits the SubmittableExtrinsic with optional resolution and rejection criteria. @@ -192,13 +208,7 @@ export async function signAndSubmitTx( ...opts }: Partial & Partial<{ tip: AnyNumber }> = {} ): Promise { - const signedTx = - 'address' in signer - ? await tx.signAsync(signer, { tip }) - : await tx.signAsync(signer.id, { - tip, - signer: Signers.getPolkadotSigner([signer]), - }) + const signedTx = await signTx(tx, signer, { tip }) return submitSignedTx(signedTx, opts) } diff --git a/packages/config/package.json b/packages/config/package.json index 11f44ef6a..aaa056fc6 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,6 +1,6 @@ { "name": "@kiltprotocol/config", - "version": "0.100.0-alpha.1", + "version": "0.100.0-alpha.2", "description": "", "type": "commonjs", "main": "./lib/index.js", diff --git a/packages/credentials/package.json b/packages/credentials/package.json index 337abc0fc..1d59ef877 100644 --- a/packages/credentials/package.json +++ b/packages/credentials/package.json @@ -1,6 +1,6 @@ { "name": "@kiltprotocol/credentials", - "version": "0.100.0-alpha.1", + "version": "0.100.0-alpha.2", "description": "", "main": "./lib/cjs/index.js", "module": "./lib/esm/index.js", @@ -41,10 +41,10 @@ "@kiltprotocol/chain-helpers": "workspace:*", "@kiltprotocol/config": "workspace:*", "@kiltprotocol/did": "workspace:*", - "@kiltprotocol/eddsa-jcs-2022": "0.1.0-rc.3", - "@kiltprotocol/es256k-jcs-2023": "0.1.0-rc.3", - "@kiltprotocol/jcs-data-integrity-proofs-common": "0.1.0-rc.3", - "@kiltprotocol/sr25519-jcs-2023": "0.1.0-rc.3", + "@kiltprotocol/eddsa-jcs-2022": "0.1.0-rc.4", + "@kiltprotocol/es256k-jcs-2023": "0.1.0-rc.4", + "@kiltprotocol/jcs-data-integrity-proofs-common": "0.1.0-rc.4", + "@kiltprotocol/sr25519-jcs-2023": "0.1.0-rc.4", "@kiltprotocol/types": "workspace:*", "@kiltprotocol/utils": "workspace:*", "@polkadot/api": "^12.0.0", diff --git a/packages/credentials/src/V1/KiltAttestationProofV1.ts b/packages/credentials/src/V1/KiltAttestationProofV1.ts index 3dc4995c2..9198e7069 100644 --- a/packages/credentials/src/V1/KiltAttestationProofV1.ts +++ b/packages/credentials/src/V1/KiltAttestationProofV1.ts @@ -5,7 +5,17 @@ * found in the LICENSE file in the root directory of this source tree. */ +import type { ApiPromise } from '@polkadot/api' +import type { QueryableStorageEntry } from '@polkadot/api/types' +import type { Option, u64, Vec } from '@polkadot/types' +import type { + AccountId, + Extrinsic, + Hash, +} from '@polkadot/types/interfaces/types.js' +import type { IEventData } from '@polkadot/types/types' import { + hexToU8a, stringToU8a, u8aCmp, u8aConcatStrict, @@ -19,40 +29,37 @@ import { encodeAddress, randomAsU8a, } from '@polkadot/util-crypto' -import type { ApiPromise } from '@polkadot/api' -import type { QueryableStorageEntry } from '@polkadot/api/types' -import type { Option, u64, Vec } from '@polkadot/types' -import type { - AccountId, - EventRecord, - Extrinsic, - Hash, -} from '@polkadot/types/interfaces/types.js' -import type { IEventData } from '@polkadot/types/types' +import type { + FrameSystemEventRecord, + RuntimeCommonAuthorizationAuthorizationId, +} from '@kiltprotocol/augment-api' +import { Blockchain } from '@kiltprotocol/chain-helpers' +import { ConfigService } from '@kiltprotocol/config' import { authorizeTx, + fromChain, getFullDid, + signersForDid, validateDid, - fromChain, } from '@kiltprotocol/did' -import { JsonSchema, SDKErrors, Caip19, Signers } from '@kiltprotocol/utils' -import { ConfigService } from '@kiltprotocol/config' -import { Blockchain } from '@kiltprotocol/chain-helpers' import type { - FrameSystemEventRecord, - RuntimeCommonAuthorizationAuthorizationId, -} from '@kiltprotocol/augment-api' -import type { - DidDocument, Did, ICType, IDelegationNode, KiltAddress, - SignerInterface, + SharedArguments, } from '@kiltprotocol/types' -import * as CType from '../ctype/index.js' +import { Caip19, JsonSchema, SDKErrors, Signers } from '@kiltprotocol/utils' +import { CTypeLoader } from '../ctype/CTypeLoader.js' +import * as CType from '../ctype/index.js' +import { + IssuerOptions, + SimplifiedTransactionResult, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + SubmitOverride, +} from '../interfaces.js' import { DEFAULT_CREDENTIAL_CONTEXTS, validateStructure as validateCredentialStructure, @@ -60,26 +67,25 @@ import { } from './KiltCredentialV1.js' import { fromGenesisAndRootHash } from './KiltRevocationStatusV1.js' import { - jsonLdExpandCredentialSubject, - ExpandedContents, - delegationIdFromAttesterDelegation, - getDelegationNodeIdForCredential, assertMatchingConnection, credentialIdFromRootHash, credentialIdToRootHash, - KILT_CREDENTIAL_IRI_PREFIX, + delegationIdFromAttesterDelegation, + ExpandedContents, + getDelegationNodeIdForCredential, + jsonLdExpandCredentialSubject, KILT_ATTESTER_DELEGATION_V1_TYPE, KILT_ATTESTER_LEGITIMATION_V1_TYPE, + KILT_CREDENTIAL_IRI_PREFIX, spiritnetGenesisHash, } from './common.js' +import { KiltRevocationStatusV1 } from './index.js' import type { CredentialSubject, KiltAttestationProofV1, KiltAttesterLegitimationV1, KiltCredentialV1, } from './types.js' -import { CTypeLoader } from '../ctype/CTypeLoader.js' -import { KiltRevocationStatusV1 } from './index.js' export type Interface = KiltAttestationProofV1 @@ -659,42 +665,41 @@ export function finalizeProof( } } -export interface TransactionResult { - status: 'InBlock' | 'Finalized' - includedAt: { blockHash: Uint8Array; blockHeight?: BigInt; blockTime?: Date } - events?: EventRecord[] -} +async function defaultTxSubmit({ + didDocument, + call, + signers, + submitter, +}: Omit & { + call: Extrinsic + submitter: KiltAddress +}): Promise { + let extrinsic = await authorizeTx( + didDocument, + call, + await signersForDid(didDocument, ...signers), + submitter + ) -type CustomHandlers = { - authorizeTx: (tx: Extrinsic) => Promise - submitTx: (tx: Extrinsic) => Promise -} -type SignersAndSubmitter = { - submitterAccount: KiltAddress - signers: readonly SignerInterface[] -} -export type IssueOpts = - | (CustomHandlers & Partial) - | (Partial & SignersAndSubmitter) - -async function defaultTxSubmit( - tx: Extrinsic, - submitterAccount: KiltAddress, - signers: readonly SignerInterface[], - api: ApiPromise -): Promise { - const extrinsic = api.tx(tx) - const signed = extrinsic.isSigned - ? extrinsic - : await extrinsic.signAsync(submitterAccount, { - signer: Signers.getPolkadotSigner(signers), - }) - const result = await Blockchain.submitSignedTx(signed, { + if (!extrinsic.isSigned) { + const accountSigners = ( + await Promise.all( + signers.map((keypair) => + 'algorithm' in keypair + ? [keypair] + : Signers.getSignersForKeypair({ keypair }) + ) + ) + ).flat() + extrinsic = await extrinsic.signAsync(submitter, { + signer: Signers.getPolkadotSigner(accountSigners), + }) + } + const result = await Blockchain.submitSignedTx(extrinsic, { resolveOn: Blockchain.IS_FINALIZED, }) const blockHash = result.status.asFinalized - const { events } = result - return { status: 'Finalized', includedAt: { blockHash }, events } + return { block: { hash: blockHash.toHex() } } } /** @@ -702,71 +707,57 @@ async function defaultTxSubmit( * Creates a complete {@link KiltAttestationProofV1} for issuing a new credential. * * @param credential A {@link KiltCredentialV1} for which a proof shall be created. - * @param issuer The DID or DID Document of the DID acting as the issuer. - * @param options Additional parameters. - * @param options.signers An array of signer interfaces related to the issuer's keys. The function selects the appropriate handlers for all signatures required for issuance (e.g., authorizing the on-chain anchoring of the credential). - * This can be omitted if both a custom authorizeTx & submitTx are given. - * @param options.submitterAccount The account which counter-signs the transaction to cover the transaction fees. - * Can be omitted if both a custom authorizeTx & submitTx are given. - * @param options.authorizeTx Allows overriding the function that takes a transaction and adds authorization by signing it with keys associated with the issuer DID. - * @param options.submitTx Allows overriding the function that takes the DID-signed transaction and submits it to a blockchain node, tracking its inclusion in a block. - * It is expected to at least return the hash of the block at which the transaction was processed. + * @param issuer Parameters describing the credential issuer. + * @param issuer.didDocument The DID Document of the DID acting as the issuer. + * @param issuer.signers An array of signer interfaces related to the issuer's keys. The function selects the appropriate handlers for all signatures required for issuance (e.g., authorizing the on-chain anchoring of the credential). + * @param issuer.submitter Either the account which counter-signs the transaction to cover the transaction fees, + * _OR_ a user-provided implementation did-authorization and submitting logic following the {@link SubmitOverride} interface. * @returns The credential where `id`, `credentialStatus`, and `issuanceDate` have been updated based on the on-chain attestation record, containing a finalized proof. */ export async function issue( credential: Omit, - issuer: Did | DidDocument, - options: IssueOpts + issuer: IssuerOptions ): Promise { + const { didDocument, signers, submitter } = issuer const updatedCredential = { ...credential, - issuer: typeof issuer === 'string' ? issuer : issuer.id, + issuer: didDocument.id, } const [proof, callArgs] = initializeProof(updatedCredential) const api = ConfigService.get('api') const call = api.tx.attestation.add(...callArgs) - const { + const args: Pick & { + call: Extrinsic + } = { + didDocument, signers, - submitterAccount, - authorizeTx: customAuthorizeTx, - submitTx: customSubmitTx, - ...otherParams - } = options - - if ( - !(customAuthorizeTx && customSubmitTx) && - !(signers && submitterAccount) - ) { - throw new Error( - '`signers` and `submitterAccount` are required options if authorizeTx or submitTx are not given' - ) + api, + call, } - - /* eslint-disable @typescript-eslint/no-non-null-assertion -- we've checked the appropriate combination of parameters above, but typescript does not follow */ - const didSigned = customAuthorizeTx - ? await customAuthorizeTx(call) - : await authorizeTx(issuer, call, signers!, submitterAccount!, otherParams) - - const transactionPromise = customSubmitTx - ? customSubmitTx(didSigned) - : defaultTxSubmit(didSigned, submitterAccount!, signers!, api) - /* eslint-enable @typescript-eslint/no-non-null-assertion */ - - const { - status, - includedAt: { blockHash, blockTime }, - } = await transactionPromise - - if (status !== 'Finalized' && status !== 'InBlock') { - throw new SDKErrors.SDKError( - `Unexpected transaction status ${status}; the transaction should be "InBlock" or "Finalized" for issuance to continue` - ) + const transactionPromise = + typeof submitter === 'function' + ? submitter(args) + : defaultTxSubmit({ + ...args, + submitter, + }) + + let result = await transactionPromise + if ('status' in result) { + if (result.status !== 'confirmed') { + throw new SDKErrors.SDKError( + `Unexpected transaction status ${result.status}; the transaction should be "confirmed" for issuance to continue` + ) + } + result = result.asConfirmed } - const timestamp = - blockTime ?? - new Date((await api.query.timestamp.now.at(blockHash)).toNumber()) + const blockHash = hexToU8a(result.block.hash) + + const timestamp = new Date( + (await (await api.at(blockHash)).query.timestamp.now()).toNumber() + ) return finalizeProof(updatedCredential, proof, { blockHash, timestamp, diff --git a/packages/credentials/src/holder.ts b/packages/credentials/src/holder.ts index ef981f433..73cdf026e 100644 --- a/packages/credentials/src/holder.ts +++ b/packages/credentials/src/holder.ts @@ -10,6 +10,7 @@ import type { CryptoSuite } from '@kiltprotocol/jcs-data-integrity-proofs-common import { Did } from '@kiltprotocol/types' import { SDKErrors } from '@kiltprotocol/utils' +import { signersForDid } from '@kiltprotocol/did' import { KiltAttestationProofV1, KiltCredentialV1 } from './V1/index.js' import { VerifiableCredential, VerifiablePresentation } from './V1/types.js' @@ -159,13 +160,13 @@ export async function createPresentation( now?: Date } = {} ): Promise { - const { did, didDocument, signers } = holder + const { didDocument, signers } = holder const { validFrom, validUntil, verifier } = presentationOptions const { proofPurpose, proofType, challenge, domain, now } = proofOptions let presentation = await Presentation.create({ credentials, - holder: did, + holder: didDocument.id, validFrom: validFrom ?? now, validUntil, verifier, @@ -184,8 +185,8 @@ export async function createPresentation( presentation = await DataIntegrity.signWithDid({ document: presentation, - signerDid: didDocument ?? did, - signers, + signerDid: didDocument, + signers: await signersForDid(didDocument, ...signers), proofPurpose, challenge, domain, diff --git a/packages/credentials/src/interfaces.ts b/packages/credentials/src/interfaces.ts index b2033729a..9494aec61 100644 --- a/packages/credentials/src/interfaces.ts +++ b/packages/credentials/src/interfaces.ts @@ -5,20 +5,41 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { SignerInterface } from '@kiltprotocol/jcs-data-integrity-proofs-common' -import type { Did, DidDocument } from '@kiltprotocol/types' +import type { Extrinsic } from '@polkadot/types/interfaces' + +import type { + DidDocument, + DidHelpersAcceptedSigners, + HexString, + KiltAddress, + SharedArguments, + TransactionResult, +} from '@kiltprotocol/types' + import type { Proof, VerifiableCredential } from './V1/types' -import type { IssueOpts } from './V1/KiltAttestationProofV1' export type SecuredDocument = { proof: Proof[] | Proof } export interface HolderOptions { - did: Did - didDocument?: DidDocument - signers: SignerInterface[] + didDocument: DidDocument + signers: DidHelpersAcceptedSigners[] +} + +export interface SimplifiedTransactionResult { + block: { hash: HexString } +} + +export type SubmitOverride = ( + args: Pick & { + call: Extrinsic + } +) => Promise + +interface SubmitterAddressOrOverride { + submitter: KiltAddress | SubmitOverride } -export type IssuerOptions = HolderOptions & IssueOpts +export type IssuerOptions = HolderOptions & SubmitterAddressOrOverride export interface VerificationResult { verified: boolean diff --git a/packages/credentials/src/issuer.ts b/packages/credentials/src/issuer.ts index 02df23d72..e1107cfd5 100644 --- a/packages/credentials/src/issuer.ts +++ b/packages/credentials/src/issuer.ts @@ -13,15 +13,14 @@ import type { UnsignedVc, VerifiableCredential } from './V1/types.js' import type { CTypeLoader } from './ctype/index.js' import type { IssuerOptions } from './interfaces.js' +export type { IssuerOptions } + /** * Creates a new credential document as a basis for issuing a credential. * This document can be shown to users as a preview or be extended with additional properties before moving on to the second step of credential issuance: * Adding a `proof` to the document using the {@link issue} function to make the credential verifiable. * * @param arguments Object holding all arguments for credential creation. - * @param arguments.type A type string identifying the (sub-)type of Verifiable Credential to be created. - * This is added to the `type` field on the credential and determines the `credentialSchema` as well. - * Defaults to the type {@link KiltCredentialV1.CREDENTIAL_TYPE KiltCredentialV1} which, for the time being, is also the only type supported. * @param arguments.issuer The Decentralized Identifier (DID) of the identity acting as the authority issuing this credential. * @param arguments.credentialSubject An object containing key-value pairs that represent claims made about the subject of the credential. * @param arguments.credentialSubject.id The DID identifying the subject of the credential, about which claims are made (the remaining key-value pairs). @@ -29,20 +28,23 @@ import type { IssuerOptions } from './interfaces.js' * Each Kilt credential is based on exactly one of these subtypes. This argument is therefore mandatory and expects the schema definition of a CType. * @param arguments.cTypeDefinitions Some CTypes are themselves composed of definitions taken from other CTypes; in that case, these definitions need to be supplied here. * Alternatively, you can set a {@link CTypeLoader} function that takes care of fetching all required definitions. + * @param arguments.type A type string identifying the (sub-)type of Verifiable Credential to be created. + * This is added to the `type` field on the credential and determines the `credentialSchema` as well. + * Defaults to the type {@link KiltCredentialV1.CREDENTIAL_TYPE KiltCredentialV1} which, for the time being, is also the only type supported. * @returns A (potentially only partial) credential that is yet to be finalized and made verifiable with a proof. */ export async function createCredential({ - type, issuer, credentialSubject, cType, cTypeDefinitions, + type, }: { - type?: string issuer: Did credentialSubject: Record & { id: Did } cType: ICType cTypeDefinitions?: ICType[] | CTypeLoader + type?: string }): Promise { switch (type) { case undefined: @@ -120,11 +122,8 @@ export async function issue( switch (proofType) { case undefined: case KiltAttestationProofV1.PROOF_TYPE: { - const { didDocument, did } = issuer - const cred = await KiltAttestationProofV1.issue( credential as KiltCredentialV1.Interface, - didDocument ?? did, issuer ) diff --git a/packages/did/package.json b/packages/did/package.json index 08a2fd209..2437a65e6 100644 --- a/packages/did/package.json +++ b/packages/did/package.json @@ -1,6 +1,6 @@ { "name": "@kiltprotocol/did", - "version": "0.100.0-alpha.1", + "version": "0.100.0-alpha.2", "description": "", "main": "./lib/cjs/index.js", "module": "./lib/esm/index.js", @@ -40,15 +40,13 @@ "@digitalbazaar/multikey-context": "^2.0.1", "@digitalbazaar/security-context": "^1.0.1", "@kiltprotocol/config": "workspace:*", - "@kiltprotocol/jcs-data-integrity-proofs-common": "0.1.0-rc.3", "@kiltprotocol/types": "workspace:*", "@kiltprotocol/utils": "workspace:*", "@polkadot/api": "^12.0.0", "@polkadot/keyring": "^13.0.0", "@polkadot/types": "^12.0.0", "@polkadot/util": "^13.0.0", - "@polkadot/util-crypto": "^13.0.0", - "varint": "^6.0.0" + "@polkadot/util-crypto": "^13.0.0" }, "peerDependenciesMeta": { "@kiltprotocol/augment-api": { diff --git a/packages/did/src/Did.chain.ts b/packages/did/src/Did.chain.ts index 079023915..967b58ebb 100644 --- a/packages/did/src/Did.chain.ts +++ b/packages/did/src/Did.chain.ts @@ -8,6 +8,7 @@ import type { Option } from '@polkadot/types' import type { AccountId32, Extrinsic, Hash } from '@polkadot/types/interfaces' import type { AnyNumber } from '@polkadot/types/types' + import type { DidDidDetails, DidDidDetailsDidAuthorizedCallOperation, @@ -15,6 +16,7 @@ import type { DidServiceEndpointsDidEndpoint, KiltSupportDeposit, } from '@kiltprotocol/augment-api' +import { ConfigService } from '@kiltprotocol/config' import type { BN, Deposit, @@ -26,23 +28,27 @@ import type { SubmittableExtrinsic, UriFragment, } from '@kiltprotocol/types' -import { ConfigService } from '@kiltprotocol/config' -import { Crypto, SDKErrors, Signers, ss58Format } from '@kiltprotocol/utils' +import { + Crypto, + Multikey, + SDKErrors, + Signers, + ss58Format, +} from '@kiltprotocol/utils' +import { + getAddressFromVerificationMethod, + getFullDid, + parse, +} from './Did.utils.js' import type { DidEncryptionMethodType, - NewService, DidSigningMethodType, - NewDidVerificationKey, NewDidEncryptionKey, + NewDidVerificationKey, + NewService, } from './DidDetails/DidDetails.js' import { isValidVerificationMethodType } from './DidDetails/DidDetails.js' -import { - keypairToMultibaseKey, - getAddressFromVerificationMethod, - getFullDid, - parse, -} from './Did.utils.js' import { type NewLightDidVerificationKey, createLightDidDocument, @@ -395,9 +401,9 @@ export async function getStoreTx( } const [authenticationKey] = authentication - const did = getAddressFromVerificationMethod({ - publicKeyMultibase: keypairToMultibaseKey(authenticationKey), - }) + const did = getAddressFromVerificationMethod( + Multikey.encodeMultibaseKeypair(authenticationKey) + ) const newAttestationKey = assertionMethod && diff --git a/packages/did/src/Did.signature.spec.ts b/packages/did/src/Did.signature.spec.ts index 79fc31936..cce8f3423 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -14,7 +14,7 @@ import type { SignerInterface, } from '@kiltprotocol/types' -import { Crypto, SDKErrors, Signers } from '@kiltprotocol/utils' +import { Crypto, Multikey, SDKErrors, Signers } from '@kiltprotocol/utils' import { randomAsHex, randomAsU8a } from '@polkadot/util-crypto' import type { NewLightDidVerificationKey } from './DidDetails' @@ -26,7 +26,7 @@ import { verifyDidSignature, } from './Did.signature' import { resolve } from './DidResolver/DidResolver' -import { keypairToMultibaseKey, multibaseKeyToDidKey, parse } from './Did.utils' +import { multibaseKeyToDidKey, parse } from './Did.utils' import { createLightDidDocument } from './DidDetails' jest.mock('./DidResolver/DidResolver') @@ -292,7 +292,8 @@ describe('full DID', () => { { controller: `did:kilt:${keypair.address}`, id: `did:kilt:${keypair.address}#0x12345`, - publicKeyMultibase: keypairToMultibaseKey(keypair), + publicKeyMultibase: + Multikey.encodeMultibaseKeypair(keypair).publicKeyMultibase, type: 'Multikey', }, ], diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index 97491e5c6..ea2d8f560 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -5,17 +5,23 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { isHex } from '@polkadot/util' +import { isHex, u8aEq } from '@polkadot/util' +import type { Keypair } from '@polkadot/util-crypto/types' import type { - DidSignature, Did, + DidDocument, + DidSignature, DidUrl, + KeyringPair, + MultibaseKeyPair, SignatureVerificationRelationship, + SignerInterface, } from '@kiltprotocol/types' -import { Crypto, SDKErrors } from '@kiltprotocol/utils' +import { Crypto, SDKErrors, Signers } from '@kiltprotocol/utils' +import { decodeAddress, isAddress } from '@polkadot/util-crypto' import { multibaseKeyToDidKey, parse, validateDid } from './Did.utils.js' import { resolve } from './DidResolver/DidResolver.js' @@ -175,3 +181,71 @@ export function signatureFromJson(input: DidSignature | LegacyDidSignature): { const signature = Crypto.coToUInt8(input.signature) return { signature, signerUrl } } + +/** + * Creates signers for {@link Did} based on a {@link DidDocument} and one or more {@link Keypair} / {@link KeyringPair}. + * + * @param didDocument The Did's DidDocument. + * @param keypairs One or more signing key pairs. + * @returns An array of signers based on the key pair. + */ +export async function signersForDid( + didDocument: DidDocument, + ...keypairs: Array +): Promise>> { + const didKeys = didDocument.verificationMethod?.map( + ({ publicKeyMultibase, id }) => ({ + ...multibaseKeyToDidKey(publicKeyMultibase), + id, + publicKeyMultibase, + }) + ) + if (didKeys && didKeys.length !== 0) { + return ( + await Promise.all( + keypairs.map((keypair) => { + // TODO: fix typing + let keyMatches: (key: any) => boolean + if ('publicKeyMultibase' in keypair) { + keyMatches = ({ publicKeyMultibase }) => + publicKeyMultibase === keypair.publicKeyMultibase + } else if ('type' in keypair) { + keyMatches = ({ keyType, publicKey }) => + keyType === keypair.type && u8aEq(publicKey, keypair.publicKey) + } else if ('publicKey' in keypair) { + keyMatches = ({ publicKey }) => u8aEq(publicKey, keypair.publicKey) + } else if (keypair.id?.startsWith(didDocument.id)) { + keyMatches = ({ id }) => keypair.id === id + } else if (keypair.id?.startsWith('z')) { + keyMatches = ({ publicKeyMultibase }) => + publicKeyMultibase === keypair.id + } else if (isAddress(keypair.id)) { + const thisPubKey = decodeAddress(keypair.id) + keyMatches = ({ publicKey }) => u8aEq(publicKey, thisPubKey) + } else { + return [] + } + const matchingKey = didKeys.find(keyMatches) + if (matchingKey) { + const id: DidUrl = matchingKey.id.startsWith('#') + ? `${didDocument.id}${matchingKey.id}` + : (matchingKey.id as DidUrl) + if ('algorithm' in keypair) { + return { + ...keypair, + id, + } + } + return Signers.getSignersForKeypair({ + keypair, + id, + type: matchingKey.keyType, + }) + } + return [] + }) + ) + ).flat() + } + return [] +} diff --git a/packages/did/src/Did.utils.ts b/packages/did/src/Did.utils.ts index 0e46e0726..c2bc594f8 100644 --- a/packages/did/src/Did.utils.ts +++ b/packages/did/src/Did.utils.ts @@ -5,21 +5,15 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { blake2AsU8a, encodeAddress, base58Encode } from '@polkadot/util-crypto' -import { u8aConcat } from '@polkadot/util' import type { - Base58BtcMultibaseString, Did, DidUrl, - KeyringPair, KiltAddress, UriFragment, VerificationMethod, } from '@kiltprotocol/types' -import { DataUtils, SDKErrors, ss58Format } from '@kiltprotocol/utils' -// @ts-expect-error Not a typescript module -import * as varint from 'varint' -import { decodeBase58BtcMultikey } from '@kiltprotocol/jcs-data-integrity-proofs-common' +import { DataUtils, Multikey, SDKErrors, ss58Format } from '@kiltprotocol/utils' +import { blake2AsU8a, encodeAddress } from '@polkadot/util-crypto' import type { DidVerificationMethodType } from './DidDetails/DidDetails.js' import { parseDocumentFromLightDid } from './DidDetails/LightDidDetails.js' @@ -133,24 +127,8 @@ type DecodedVerificationMethod = { keyType: DidVerificationMethodType } -const MULTICODEC_ECDSA_PREFIX = 0xe7 -const MULTICODEC_X25519_PREFIX = 0xec -const MULTICODEC_ED25519_PREFIX = 0xed -const MULTICODEC_SR25519_PREFIX = 0xef - -const multicodecPrefixes: Record = - { - [MULTICODEC_ECDSA_PREFIX]: ['ecdsa', 33], - [MULTICODEC_X25519_PREFIX]: ['x25519', 32], - [MULTICODEC_ED25519_PREFIX]: ['ed25519', 32], - [MULTICODEC_SR25519_PREFIX]: ['sr25519', 32], - } -const multicodecReversePrefixes: Record = { - ecdsa: MULTICODEC_ECDSA_PREFIX, - x25519: MULTICODEC_X25519_PREFIX, - ed25519: MULTICODEC_ED25519_PREFIX, - sr25519: MULTICODEC_SR25519_PREFIX, -} +const KEY_LENGTH_ECDSA = 33 +const KEY_LENGTH_OTHER = 32 /** * Decode a Multikey representation of a verification method into its fundamental components: the public key and the key type. @@ -161,68 +139,20 @@ const multicodecReversePrefixes: Record = { export function multibaseKeyToDidKey( publicKeyMultibase: VerificationMethod['publicKeyMultibase'] ): DecodedVerificationMethod { - const { keyBytes, prefix } = decodeBase58BtcMultikey(publicKeyMultibase) - - const [keyType, expectedPublicKeyLength] = multicodecPrefixes[prefix] - if (keyType === undefined) { + const { publicKey, type } = Multikey.decodeMultibaseKeypair({ + publicKeyMultibase, + }) + const expectedPublicKeyLength = + type === 'secp256k1' ? KEY_LENGTH_ECDSA : KEY_LENGTH_OTHER + if (publicKey.length !== expectedPublicKeyLength) { throw new SDKErrors.DidError( - `Cannot decode key type for multibase key "${publicKeyMultibase}".` - ) - } - if (keyBytes.length !== expectedPublicKeyLength) { - throw new SDKErrors.DidError( - `Key of type "${keyType}" is expected to be ${expectedPublicKeyLength} bytes long. Provided key is ${keyBytes.length} bytes long instead.` + `Key of type "${type}" is expected to be ${expectedPublicKeyLength} bytes long. Provided key is ${publicKey.length} bytes long instead.` ) } return { - keyType, - publicKey: keyBytes, - } -} - -// TODO: This could also be exposed in a new release candidate of the `@kiltprotocol/jcs-data-integrity-proofs-common` package. -function multibase58BtcKeyBytesEncoding( - key: Uint8Array, - keyPrefix: number -): Base58BtcMultibaseString { - const varintEncodedPrefix = varint.encode(keyPrefix) - const prefixedKey = u8aConcat(varintEncodedPrefix, key) - const base58BtcEncodedKey = base58Encode(prefixedKey) - return `z${base58BtcEncodedKey}` -} - -/** - * Calculate the Multikey representation of a keypair given its type and public key. - * - * @param keypair The input keypair to encode as Multikey. - * @param keypair.type The keypair {@link DidVerificationMethodType}. - * @param keypair.publicKey The keypair public key. - * @returns The Multikey representation (i.e., multicodec-prefixed, then multibase encoded) of the provided keypair. - */ -export function keypairToMultibaseKey({ - type, - publicKey, -}: Pick & { - type: DidVerificationMethodType -}): VerificationMethod['publicKeyMultibase'] { - const multiCodecPublicKeyPrefix = multicodecReversePrefixes[type] - if (multiCodecPublicKeyPrefix === undefined) { - throw new SDKErrors.DidError( - `The provided key type "${type}" is not supported.` - ) - } - const expectedPublicKeySize = multicodecPrefixes[multiCodecPublicKeyPrefix][1] - if (publicKey.length !== expectedPublicKeySize) { - throw new SDKErrors.DidError( - `Key of type "${type}" is expected to be ${expectedPublicKeySize} bytes long. Provided key is ${publicKey.length} bytes long instead.` - ) - } - const prefixedEncodedPublicKey = multibase58BtcKeyBytesEncoding( + keyType: type === 'secp256k1' ? 'ecdsa' : type, publicKey, - multiCodecPublicKeyPrefix - ) - - return prefixedEncodedPublicKey + } } /** @@ -240,28 +170,16 @@ export function didKeyToVerificationMethod( id: IdType, { keyType, publicKey }: DecodedVerificationMethod ): VerificationMethod { - const multiCodecPublicKeyPrefix = multicodecReversePrefixes[keyType] - if (multiCodecPublicKeyPrefix === undefined) { - throw new SDKErrors.DidError( - `Provided key type "${keyType}" not supported.` - ) - } - const expectedPublicKeySize = multicodecPrefixes[multiCodecPublicKeyPrefix][1] - if (publicKey.length !== expectedPublicKeySize) { - throw new SDKErrors.DidError( - `Key of type "${keyType}" is expected to be ${expectedPublicKeySize} bytes long. Provided key is ${publicKey.length} bytes long instead.` - ) - } - const prefixedEncodedPublicKey = multibase58BtcKeyBytesEncoding( + const { publicKeyMultibase } = Multikey.encodeMultibaseKeypair({ publicKey, - multiCodecPublicKeyPrefix - ) + type: keyType, + }) return { controller, id, type: 'Multikey', - publicKeyMultibase: prefixedEncodedPublicKey, + publicKeyMultibase, } } @@ -340,7 +258,8 @@ export function getAddressFromVerificationMethod({ // Otherwise it’s ecdsa. // Taken from https://github.com/polkadot-js/common/blob/master/packages/keyring/src/pair/index.ts#L44 - const address = publicKey.length > 32 ? blake2AsU8a(publicKey) : publicKey + const address = + publicKey.length > KEY_LENGTH_OTHER ? blake2AsU8a(publicKey) : publicKey return encodeAddress(address, ss58Format) } diff --git a/packages/did/src/DidDetails/LightDidDetails.spec.ts b/packages/did/src/DidDetails/LightDidDetails.spec.ts index 66b704dd3..61332a0f4 100644 --- a/packages/did/src/DidDetails/LightDidDetails.spec.ts +++ b/packages/did/src/DidDetails/LightDidDetails.spec.ts @@ -6,13 +6,12 @@ */ import type { DidDocument, Did, DidUrl } from '@kiltprotocol/types' - -import { Crypto } from '@kiltprotocol/utils' +import { Crypto, Multikey } from '@kiltprotocol/utils' import type { NewService } from './DidDetails.js' import type { CreateDocumentInput } from './LightDidDetails.js' -import { keypairToMultibaseKey, parse } from '../Did.utils.js' +import { parse } from '../Did.utils.js' import { createLightDidDocument, parseDocumentFromLightDid, @@ -62,19 +61,19 @@ describe('When creating an instance from the details', () => { { controller: did, id: `${did}#authentication`, - publicKeyMultibase: keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ publicKey: authKey.publicKey, type: 'sr25519', - }), + }).publicKeyMultibase, type: 'Multikey', }, { controller: did, id: `${did}#encryption`, - publicKeyMultibase: keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ publicKey: encKey.publicKey, type: 'x25519', - }), + }).publicKeyMultibase, type: 'Multikey', }, ], @@ -115,19 +114,19 @@ describe('When creating an instance from the details', () => { { controller: did, id: `${did}#authentication`, - publicKeyMultibase: keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ publicKey: authKey.publicKey, type: 'ed25519', - }), + }).publicKeyMultibase, type: 'Multikey', }, { controller: did, id: `${did}#encryption`, - publicKeyMultibase: keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ publicKey: encKey.publicKey, type: 'x25519', - }), + }).publicKeyMultibase, type: 'Multikey', }, ], @@ -197,19 +196,19 @@ describe('When creating an instance from a light DID', () => { { controller: did, id: `${did}#authentication`, - publicKeyMultibase: keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ publicKey: authKey.publicKey, type: 'sr25519', - }), + }).publicKeyMultibase, type: 'Multikey', }, { controller: did, id: `${did}#encryption`, - publicKeyMultibase: keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ publicKey: encKey.publicKey, type: 'x25519', - }), + }).publicKeyMultibase, type: 'Multikey', }, ], diff --git a/packages/did/src/DidDetails/LightDidDetails.ts b/packages/did/src/DidDetails/LightDidDetails.ts index f92d68044..d08705569 100644 --- a/packages/did/src/DidDetails/LightDidDetails.ts +++ b/packages/did/src/DidDetails/LightDidDetails.ts @@ -5,29 +5,27 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { DidDocument, Did } from '@kiltprotocol/types' - import { base58Decode, base58Encode, decodeAddress, } from '@polkadot/util-crypto' -import { cbor, SDKErrors, ss58Format } from '@kiltprotocol/utils' -import type { - NewDidEncryptionKey, - NewDidVerificationKey, - NewService, - DidSigningMethodType, -} from './DidDetails.js' +import type { Did, DidDocument } from '@kiltprotocol/types' +import { cbor, Multikey, SDKErrors, ss58Format } from '@kiltprotocol/utils' +import { urlFragmentToChain, validateNewService } from '../Did.chain.js' import { - keypairToMultibaseKey, didKeyToVerificationMethod, getAddressFromVerificationMethod, parse, } from '../Did.utils.js' -import { urlFragmentToChain, validateNewService } from '../Did.chain.js' +import type { + DidSigningMethodType, + NewDidEncryptionKey, + NewDidVerificationKey, + NewService, +} from './DidDetails.js' import { addKeypairAsVerificationMethod, encryptionMethodTypes, @@ -231,9 +229,9 @@ export function createLightDidDocument({ // Validity is checked in validateCreateDocumentInput const authenticationKeyTypeEncoding = verificationKeyTypeToLightDidEncoding[authentication[0].type] - const address = getAddressFromVerificationMethod({ - publicKeyMultibase: keypairToMultibaseKey(authentication[0]), - }) + const address = getAddressFromVerificationMethod( + Multikey.encodeMultibaseKeypair(authentication[0]) + ) const encodedDetailsString = encodedDetails ? `:${encodedDetails}` : '' const did = diff --git a/packages/did/src/DidDetails/index.ts b/packages/did/src/DidDetails/index.ts index a80411c9d..8f61d85cf 100644 --- a/packages/did/src/DidDetails/index.ts +++ b/packages/did/src/DidDetails/index.ts @@ -19,6 +19,7 @@ export type { export { isValidDidVerificationType, isValidEncryptionMethodType, + signingMethodTypes, } from './DidDetails.js' export * from './LightDidDetails.js' export * from './FullDidDetails.js' diff --git a/packages/did/src/DidResolver/DidResolver.spec.ts b/packages/did/src/DidResolver/DidResolver.spec.ts index a907c0cf6..2548e3947 100644 --- a/packages/did/src/DidResolver/DidResolver.spec.ts +++ b/packages/did/src/DidResolver/DidResolver.spec.ts @@ -17,7 +17,7 @@ import { VerificationMethod, UriFragment, } from '@kiltprotocol/types' -import { Crypto, cbor } from '@kiltprotocol/utils' +import { Crypto, Multikey, cbor } from '@kiltprotocol/utils' import { stringToU8a } from '@polkadot/util' import { ApiMocks, makeSigningKeyTool } from '../../../../tests/testUtils' @@ -87,10 +87,10 @@ function generateAuthenticationVerificationMethod( id: `${controller}#auth`, controller, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ publicKey: new Uint8Array(32).fill(0), type: 'ed25519', - }), + }).publicKeyMultibase, } } @@ -101,10 +101,10 @@ function generateEncryptionVerificationMethod( id: `${controller}#enc`, controller, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ publicKey: new Uint8Array(32).fill(1), type: 'x25519', - }), + }).publicKeyMultibase, } } @@ -115,10 +115,10 @@ function generateAssertionVerificationMethod( id: `${controller}#att`, controller, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ publicKey: new Uint8Array(32).fill(2), type: 'sr25519', - }), + }).publicKeyMultibase, } } @@ -129,10 +129,10 @@ function generateCapabilityDelegationVerificationMethod( id: `${controller}#del`, controller, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ publicKey: new Uint8Array(33).fill(3), type: 'ecdsa', - }), + }).publicKeyMultibase, } } @@ -313,10 +313,10 @@ describe('When resolving a full DID', () => { controller: fullDidWithAuthenticationKey, id: `${fullDidWithAuthenticationKey}#auth`, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ type: 'ed25519', publicKey: new Uint8Array(32).fill(0), - }), + }).publicKeyMultibase, }, ], }, @@ -363,37 +363,37 @@ describe('When resolving a full DID', () => { controller: fullDidWithAllKeys, id: `${fullDidWithAllKeys}#auth`, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ type: 'ed25519', publicKey: new Uint8Array(32).fill(0), - }), + }).publicKeyMultibase, }, { controller: fullDidWithAllKeys, id: `${fullDidWithAllKeys}#enc`, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ type: 'x25519', publicKey: new Uint8Array(32).fill(1), - }), + }).publicKeyMultibase, }, { controller: fullDidWithAllKeys, id: `${fullDidWithAllKeys}#att`, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ type: 'sr25519', publicKey: new Uint8Array(32).fill(2), - }), + }).publicKeyMultibase, }, { controller: fullDidWithAllKeys, id: `${fullDidWithAllKeys}#del`, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ type: 'ecdsa', publicKey: new Uint8Array(33).fill(3), - }), + }).publicKeyMultibase, }, ], }, @@ -435,10 +435,10 @@ describe('When resolving a full DID', () => { controller: fullDidWithServiceEndpoints, id: `${fullDidWithServiceEndpoints}#auth`, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ type: 'ed25519', publicKey: new Uint8Array(32).fill(0), - }), + }).publicKeyMultibase, }, ], service: [ @@ -488,10 +488,10 @@ describe('When resolving a full DID', () => { controller: didWithAuthenticationKey, id: `${didWithAuthenticationKey}#auth`, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ type: 'ed25519', publicKey: new Uint8Array(32).fill(0), - }), + }).publicKeyMultibase, }, ], alsoKnownAs: ['w3n:w3nick'], @@ -507,9 +507,9 @@ describe('When resolving a full DID', () => { augmentedApi.createType('Option', null) ) const randomKeypair = (await makeSigningKeyTool()).authentication[0] - const randomDid = Did.getFullDidFromVerificationMethod({ - publicKeyMultibase: Did.keypairToMultibaseKey(randomKeypair), - }) + const randomDid = Did.getFullDidFromVerificationMethod( + Multikey.encodeMultibaseKeypair(randomKeypair) + ) expect(await Did.resolve(randomDid)).toStrictEqual({ didDocumentMetadata: {}, didResolutionMetadata: { error: 'notFound' }, @@ -563,10 +563,10 @@ describe('When resolving a light DID', () => { controller: lightDidWithAuthenticationKey.id, id: `${lightDidWithAuthenticationKey.id}#authentication`, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ ...authKey, type: 'sr25519', - }), + }).publicKeyMultibase, }, ], }, @@ -594,16 +594,17 @@ describe('When resolving a light DID', () => { controller: lightDid.id, id: `${lightDid.id}#authentication`, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ ...authKey, type: 'sr25519', - }), + }).publicKeyMultibase, }, { controller: lightDid.id, id: `${lightDid.id}#encryption`, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey(encryptionKey), + publicKeyMultibase: + Multikey.encodeMultibaseKeypair(encryptionKey).publicKeyMultibase, }, ], service: [ @@ -737,10 +738,10 @@ describe('DID Resolution compliance', () => { id: `${did}#auth`, controller: did, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ publicKey: new Uint8Array(32).fill(0), type: 'ed25519', - }), + }).publicKeyMultibase, }, ], }, @@ -786,10 +787,10 @@ describe('DID Resolution compliance', () => { id: `${did}#auth`, controller: did, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ publicKey: new Uint8Array(32).fill(0), type: 'ed25519', - }), + }).publicKeyMultibase, }, ], }) @@ -817,10 +818,10 @@ describe('DID Resolution compliance', () => { id: `${did}#auth`, controller: did, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ publicKey: new Uint8Array(32).fill(0), type: 'ed25519', - }), + }).publicKeyMultibase, }, ], '@context': [Did.W3C_DID_CONTEXT_URL, Did.KILT_DID_CONTEXT_URL], @@ -847,10 +848,10 @@ describe('DID Resolution compliance', () => { id: `${did}#auth`, controller: did, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ publicKey: new Uint8Array(32).fill(0), type: 'ed25519', - }), + }).publicKeyMultibase, }, ], }) @@ -911,10 +912,10 @@ describe('DID Resolution compliance', () => { id: `${did}#auth`, controller: did, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ publicKey: new Uint8Array(32).fill(0), type: 'ed25519', - }), + }).publicKeyMultibase, }, ], }, @@ -938,10 +939,10 @@ describe('DID Resolution compliance', () => { id: `${did}#auth`, controller: did, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ publicKey: new Uint8Array(32).fill(0), type: 'ed25519', - }), + }).publicKeyMultibase, }, ], '@context': [Did.W3C_DID_CONTEXT_URL, Did.KILT_DID_CONTEXT_URL], @@ -965,10 +966,10 @@ describe('DID Resolution compliance', () => { id: `${did}#auth`, controller: did, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ publicKey: new Uint8Array(32).fill(0), type: 'ed25519', - }), + }).publicKeyMultibase, }, ], }) @@ -988,10 +989,10 @@ describe('DID Resolution compliance', () => { controller: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ publicKey: new Uint8Array(32).fill(0), type: 'ed25519', - }), + }).publicKeyMultibase, }, }) }) @@ -1072,10 +1073,10 @@ describe('DID Resolution compliance', () => { id: `${did}#auth`, controller: did, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ publicKey: new Uint8Array(32).fill(0), type: 'ed25519', - }), + }).publicKeyMultibase, }, ], }, diff --git a/packages/jsonld-suites/package.json b/packages/jsonld-suites/package.json index d2ed2da78..3b6cdd939 100644 --- a/packages/jsonld-suites/package.json +++ b/packages/jsonld-suites/package.json @@ -1,6 +1,6 @@ { "name": "@kiltprotocol/jsonld-suites", - "version": "0.100.0-alpha.1", + "version": "0.100.0-alpha.2", "description": "", "main": "./lib/cjs/index.js", "module": "./lib/esm/index.js", diff --git a/packages/jsonld-suites/src/suites/KiltAttestationProofV1.spec.ts b/packages/jsonld-suites/src/suites/KiltAttestationProofV1.spec.ts index fff6928ef..35adb0514 100644 --- a/packages/jsonld-suites/src/suites/KiltAttestationProofV1.spec.ts +++ b/packages/jsonld-suites/src/suites/KiltAttestationProofV1.spec.ts @@ -5,7 +5,7 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { u8aEq } from '@polkadot/util' +import { u8aEq, u8aToHex } from '@polkadot/util' import { base58Decode } from '@polkadot/util-crypto' import { Ed25519Signature2020, @@ -36,6 +36,7 @@ import { KiltCredentialV1, Types, W3C_CREDENTIAL_CONTEXT_URL, + type Issuer, } from '@kiltprotocol/credentials' import { @@ -448,28 +449,23 @@ describe('issuance', () => { let toBeSigned: CredentialStub const { issuer } = attestedVc - const signer: SignerInterface<'Sr25519', DidUrl> = { - sign: async () => new Uint8Array(32), - algorithm: 'Sr25519', - id: `${issuer}#1`, - } - const transactionHandler: KiltAttestationProofV1.IssueOpts = { - signers: [signer], - submitterAccount: attester, - submitTx: async () => { - return { - status: 'Finalized', - includedAt: { - blockHash, - blockTime: timestamp, - }, - } - }, - authorizeTx: async (tx) => { - txArgs = tx.args - return tx + const signers: Array> = [ + { + sign: async () => new Uint8Array(32), + algorithm: 'Sr25519', + id: `${issuer}#1`, }, + ] + + const submitter: Issuer.IssuerOptions['submitter'] = async ({ call }) => { + txArgs = call.args + return { + block: { + hash: u8aToHex(blockHash), + }, + } } + beforeEach(() => { toBeSigned = { credentialSubject: attestedVc.credentialSubject, @@ -481,8 +477,7 @@ describe('issuance', () => { let newCred: Partial = await issuanceSuite.anchorCredential( { ...toBeSigned }, - issuer, - transactionHandler + { didDocument, signers, submitter } ) newCred = await vcjs.issue({ credential: newCred, @@ -540,8 +535,7 @@ describe('issuance', () => { { ...toBeSigned, }, - issuer, - transactionHandler + { didDocument, signers, submitter } ) newCred = (await vcjs.issue({ credential: { diff --git a/packages/jsonld-suites/src/suites/KiltAttestationProofV1.ts b/packages/jsonld-suites/src/suites/KiltAttestationProofV1.ts index 7b6366dc6..783fbc4f5 100644 --- a/packages/jsonld-suites/src/suites/KiltAttestationProofV1.ts +++ b/packages/jsonld-suites/src/suites/KiltAttestationProofV1.ts @@ -15,11 +15,11 @@ import { ConfigService } from '@kiltprotocol/config' import { CType, KiltAttestationProofV1, + KiltCredentialV1, KiltRevocationStatusV1, Types, - KiltCredentialV1, } from '@kiltprotocol/credentials' -import type { DidDocument, Did, ICType } from '@kiltprotocol/types' +import type { ICType } from '@kiltprotocol/types' import { Caip2 } from '@kiltprotocol/utils' import type { DocumentLoader, JsonLdObj } from '../documentLoader.js' @@ -201,15 +201,13 @@ export class KiltAttestationV1Suite extends LinkedDataProof { * You can then add a proof about the successful attestation to the credential using `createProof`. * * @param input A partial {@link KiltCredentialV1} `credentialSubject` is required. - * @param issuer The DID Document or, alternatively, the DID of the issuer. - * @param submissionOptions Authorization and submission handlers, or alternatively signers, to be passed to {@link KiltAttestationProofV1.issue | issue} for authorizing the on-chain anchoring of the credential with the issuer's signature. + * @param issuer Parameters describing the credential issuer, such as the issuer's `didDocument` and `signers`, to be passed to {@link KiltAttestationProofV1.issue | issue}. * * @returns A copy of the input updated to fit the {@link KiltCredentialV1} and to align with the attestation record (concerns, e.g., the `issuanceDate` which is set to the block time at which the credential was anchored). */ public async anchorCredential( input: CredentialStub, - issuer: DidDocument | Did, - submissionOptions: Parameters['2'] + issuer: Parameters['1'] ): Promise> { const { credentialSubject, type } = input @@ -239,8 +237,7 @@ export class KiltAttestationV1Suite extends LinkedDataProof { const { proof, ...credential } = await KiltAttestationProofV1.issue( credentialStub, - issuer, - submissionOptions + issuer ) this.attestationInfo.set(credential.id, proof) diff --git a/packages/jsonld-suites/src/suites/Sr25519Signature2020.spec.ts b/packages/jsonld-suites/src/suites/Sr25519Signature2020.spec.ts index 8270f2b33..3ad8f540c 100644 --- a/packages/jsonld-suites/src/suites/Sr25519Signature2020.spec.ts +++ b/packages/jsonld-suites/src/suites/Sr25519Signature2020.spec.ts @@ -43,7 +43,7 @@ const documentLoader = combineDocumentLoaders([ export async function makeFakeDid() { const keypair = Crypto.makeKeypairFromUri('//Ingo', 'sr25519') - const id = ingosCredential.credentialSubject.id as KiltDid + const id = ingosCredential.issuer as KiltDid const didDocument: DidDocument = { id, authentication: [`${id}#authentication`], @@ -101,7 +101,7 @@ it('issues and verifies a signed credential', async () => { '@context': [W3C_CREDENTIAL_CONTEXT_URL] as any, type: ['VerifiableCredential'], credentialSubject: ingosCredential.credentialSubject, - issuer: ingosCredential.credentialSubject.id, + issuer: ingosCredential.issuer, } as Partial const verifiableCredential = await vcjs.issue({ diff --git a/packages/legacy-credentials/package.json b/packages/legacy-credentials/package.json index 64d9745d4..e03f1fe13 100644 --- a/packages/legacy-credentials/package.json +++ b/packages/legacy-credentials/package.json @@ -1,6 +1,6 @@ { "name": "@kiltprotocol/legacy-credentials", - "version": "0.100.0-alpha.1", + "version": "0.100.0-alpha.2", "description": "", "main": "./lib/cjs/index.js", "module": "./lib/esm/index.js", diff --git a/packages/sdk-js/package.json b/packages/sdk-js/package.json index dfa824c7b..d0c553887 100644 --- a/packages/sdk-js/package.json +++ b/packages/sdk-js/package.json @@ -1,6 +1,6 @@ { "name": "@kiltprotocol/sdk-js", - "version": "1.0.0-alpha.1", + "version": "1.0.0-alpha.2", "description": "", "main": "./lib/cjs/index.js", "module": "./lib/esm/index.js", @@ -46,7 +46,9 @@ "@kiltprotocol/credentials": "workspace:*", "@kiltprotocol/did": "workspace:*", "@kiltprotocol/type-definitions": "^0.35.0", - "@kiltprotocol/utils": "workspace:*" + "@kiltprotocol/utils": "workspace:*", + "@polkadot/api": "^12.0.0", + "@polkadot/util": "^13.0.0" }, "peerDependencies": { "@kiltprotocol/augment-api": "^1.11210.0" diff --git a/packages/sdk-js/src/DidHelpers/checkResult.ts b/packages/sdk-js/src/DidHelpers/checkResult.ts new file mode 100644 index 000000000..47fe0e5b3 --- /dev/null +++ b/packages/sdk-js/src/DidHelpers/checkResult.ts @@ -0,0 +1,313 @@ +/** + * Copyright (c) 2018-2024, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +/* eslint-disable jsdoc/require-jsdoc */ +// functions in this file are not meant to be public + +import type { ApiPromise } from '@polkadot/api' +import type { SubmittableResultValue } from '@polkadot/api/types' +import type { BlockNumber } from '@polkadot/types/interfaces' +import { u8aToHex, u8aToU8a } from '@polkadot/util' + +import type { + FrameSystemEventRecord as EventRecord, + SpRuntimeDispatchError, +} from '@kiltprotocol/augment-api' +import { resolver as DidResolver, signersForDid } from '@kiltprotocol/did' +import type { + Did, + HexString, + SharedArguments, + TransactionResult, +} from '@kiltprotocol/types' + +function mapError(err: SpRuntimeDispatchError, api: ApiPromise): Error { + if (err.isModule) { + const { docs, method, section } = api.registry.findMetaError(err.asModule) + return new Error(`${section}.${method}: ${docs}`) + } + return new Error(`${err.type}: ${err.value.toHuman()}`) +} + +function assertStatus(expected: string, actual?: string): void { + if (actual !== expected) { + const getterName = `as${expected.slice(0, 1).toUpperCase()}${expected.slice( + 1 + )}` + throw new Error(`can't access '${getterName}' when status is '${actual}'`) + } +} + +function checkEventsForErrors( + api: ApiPromise, + txEvents: EventRecord[] = [] +): Error | undefined { + let error: Error | undefined + txEvents.some(({ event }) => { + if (api.events.system.ExtrinsicFailed.is(event)) { + error = mapError(event.data[0], api) + } else if (api.events.proxy.ProxyExecuted.is(event)) { + const res = event.data[0] + if (res.isErr) { + error = mapError(res.asErr, api) + } + } else if (api.events.did.DidCallDispatched.is(event)) { + const res = event.data[1] + if (res.isErr) { + error = mapError(res.asErr, api) + } + } else if (api.events.utility.ItemFailed.is(event)) { + error = mapError(event.data[0], api) + } else if (api.events.utility.BatchInterrupted.is(event)) { + error = mapError(event.data[1], api) + } + if (typeof error !== 'undefined') { + return true + } + return false + }) + return error +} + +// Note: returns `undefined` in the case of `status === "inBlock`. +function checkStatus(result: SubmittableResultValue): { + status?: TransactionResult['status'] + error?: Error + blockHash?: HexString + blockNumber?: BigInt +} { + let blockNumber: BigInt | undefined + switch (result.status.type) { + case 'Finalized': + case 'InBlock': + if ('blockNumber' in result) { + blockNumber = (result.blockNumber as BlockNumber).toBigInt() + } + return { + status: undefined, + blockNumber, + blockHash: result.status.value.toHex(), + } + case 'Dropped': + case 'FinalityTimeout': + case 'Invalid': + case 'Usurped': + return { + status: 'rejected', + error: new Error(result.status.type), + } + case 'Broadcast': + case 'Future': + case 'Ready': + case 'Retracted': + return { + status: 'unknown', + error: new Error(result.status.type), + } + default: + return { + status: 'unknown', + error: new Error(`unknown tx status variant ${result.status.type}`), + } + } +} + +async function resolveBlockAndEvents( + blockInfo: { blockHash: HexString; txHash: HexString }, + api: ApiPromise +): Promise<{ + blockNumber: BigInt + blockHash: HexString + txEvents: EventRecord[] +}> { + const txHashHash = api.createType('Hash', blockInfo.blockHash) + const { + block: { block }, + events, + } = await api.derive.tx.events(txHashHash) + const blockNumber = block.header.number.toBigInt() + const { blockHash } = blockInfo + const txIndex = block.extrinsics.findIndex((tx) => + tx.hash.eq(blockInfo.txHash) + ) + const txEvents = events.filter( + ({ phase }) => phase.isApplyExtrinsic && phase.asApplyExtrinsic.eqn(txIndex) + ) + return { blockNumber, blockHash, txEvents } +} + +function checkIfEventsMatch( + expectedEvents: Array<{ section: string; method: string }>, + txEvents: EventRecord[] +): boolean { + return expectedEvents.every(({ section, method }) => + txEvents.some( + ({ event }) => + event.section.toLowerCase() === section.toLowerCase() && + event.method.toLowerCase() === method.toLowerCase() + ) + ) +} + +export async function checkResultImpl( + result: { blockHash: HexString; txHash: HexString } | SubmittableResultValue, + api: ApiPromise, + expectedEvents: Array<{ section: string; method: string }> = [], + did: Did, + signersOrKeys: SharedArguments['signers'] +): Promise { + let txEvents: EventRecord[] = [] + let status: TransactionResult['status'] | undefined + let error: Error | undefined + let blockHash: HexString | undefined + let blockNumber: BigInt | undefined + + // Case where `SubmittableResultValue` is provided. + if ('status' in result) { + txEvents = result.events ?? [] + ;({ status, blockHash, blockNumber, error } = checkStatus(result)) + } + // Case where block hash and tx hash are provided. + else if ( + 'blockHash' in result && + 'txHash' in result && + typeof result.blockHash === 'string' && + typeof result.txHash === 'string' + ) { + // Set blockNumber, blockHash, and the transactionEvents. + ;({ blockNumber, blockHash, txEvents } = await resolveBlockAndEvents( + result, + api + )) + } + // Type of the `result` argument is invalid. + else { + status = 'unknown' + error = new Error('missing blockHash and/or txHash') + } + + if (typeof status !== 'string') { + const eventError = checkEventsForErrors(api, txEvents) + if (eventError !== undefined) { + error = eventError + } + + const eventMatch = checkIfEventsMatch(expectedEvents, txEvents) + + status = !error && eventMatch ? 'confirmed' : 'failed' + if (!error && !eventMatch) { + error = new Error('did not find expected events') + } + } + + const { didDocument } = await DidResolver.resolve(did) + if (blockHash && !blockNumber) { + blockNumber = ( + await (await api.at(blockHash)).query.system.number() + ).toBigInt() + } + let signers: Awaited> + if (didDocument) { + signers = await signersForDid(didDocument, ...signersOrKeys) + } else if (status === 'confirmed') { + status = 'unknown' + error = new Error('failed to fetch DID document') + } + + return { + status, + get asFailed(): TransactionResult['asFailed'] { + assertStatus('failed', status) + return { + error: error ?? new Error('unknown error'), + txHash: u8aToHex(u8aToU8a(result.txHash)), + signers, + didDocument, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + block: { hash: blockHash!, number: blockNumber! }, + events: txEvents.map(({ event }) => event), + toJSON() { + const clone = { ...this } as any + clone.block = { + ...clone.block, + number: clone.block.number.toString(), + } + delete clone.toJSON + return clone + }, + } + }, + get asUnknown(): TransactionResult['asUnknown'] { + assertStatus('unknown', status) + return { + error: error as Error, + txHash: u8aToHex(u8aToU8a(result.txHash)), + } + }, + get asRejected(): TransactionResult['asRejected'] { + assertStatus('rejected', status) + return { + error: error ?? new Error('unknown error'), + txHash: u8aToHex(u8aToU8a(result.txHash)), + signers, + didDocument, + } + }, + get asConfirmed(): TransactionResult['asConfirmed'] { + assertStatus('confirmed', status) + return { + txHash: u8aToHex(u8aToU8a(result.txHash)), + signers, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + didDocument: didDocument!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + block: { hash: blockHash!, number: blockNumber! }, + events: txEvents.map(({ event }) => event), + toJSON() { + const clone = { ...this } as any + clone.block = { + ...clone.block, + number: clone.block.number.toString(), + } + delete clone.toJSON + return clone + }, + } + }, + toJSON() { + switch (status) { + case 'confirmed': { + return { + status, + value: (this as TransactionResult).asConfirmed.toJSON(), + } + } + case 'failed': { + return { + status, + value: (this as TransactionResult).asFailed.toJSON(), + } + } + case 'rejected': { + return { + status, + value: (this as TransactionResult).asRejected, + } + } + case 'unknown': { + return { + status, + value: (this as TransactionResult).asUnknown, + } + } + default: { + throw new Error('invalid status') + } + } + }, + } +} diff --git a/packages/sdk-js/src/DidHelpers/common.ts b/packages/sdk-js/src/DidHelpers/common.ts new file mode 100644 index 000000000..692425494 --- /dev/null +++ b/packages/sdk-js/src/DidHelpers/common.ts @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2018-2024, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +/* eslint-disable jsdoc/require-jsdoc */ +// functions in this file are not meant to be public + +import { Blockchain } from '@kiltprotocol/chain-helpers' +import { multibaseKeyToDidKey } from '@kiltprotocol/did' +import type { + KeyringPair, + KiltAddress, + MultibaseKeyPair, + DidHelpersAcceptedPublicKeyEncodings, + SharedArguments, + TransactionHandlers, + TransactionSigner, +} from '@kiltprotocol/types' +import { Keyring, Multikey, Crypto } from '@kiltprotocol/utils' + +export async function submitImpl( + getSubmittable: TransactionHandlers['getSubmittable'], + options: Pick & { + didNonce?: number | BigInt + awaitFinalized?: boolean + } +): ReturnType { + const submittable = await getSubmittable({ + ...options, + signSubmittable: true, + }) + + const { awaitFinalized = true } = options + const result = await Blockchain.submitSignedTx( + options.api.tx(submittable.txHex), + { + resolveOn: awaitFinalized + ? (res) => res.isFinalized || res.isError + : (res) => res.isInBlock || res.isError, + rejectOn: () => false, + } + ) + + return submittable.checkResult(result) +} + +export function convertPublicKey( + pk: DidHelpersAcceptedPublicKeyEncodings +): { + publicKey: Uint8Array + type: string +} { + let publicKey: Uint8Array + let type: string + + if (typeof pk === 'string') { + ;({ publicKey, keyType: type } = multibaseKeyToDidKey(pk)) + } else if ('publicKeyMultibase' in pk) { + ;({ publicKey, keyType: type } = multibaseKeyToDidKey( + pk.publicKeyMultibase + )) + } else if ( + 'publicKey' in pk && + pk.publicKey.constructor.name === 'Uint8Array' + ) { + ;({ publicKey, type } = pk) + } else { + throw new Error('invalid public key') + } + return { publicKey, type } +} + +export function extractSubmitterSignerAndAccount( + submitter: KeyringPair | TransactionSigner | MultibaseKeyPair | KiltAddress +): { + submitterSigner?: TransactionSigner | KeyringPair | undefined + submitterAccount: KiltAddress +} { + // KiltAddress + if (typeof submitter === 'string') { + return { + submitterAccount: submitter, + } + } + // KeyringPair + if ('address' in submitter) { + return { + submitterAccount: submitter.address as KiltAddress, + submitterSigner: submitter, + } + } + // Blockchain.TransactionSigner + if ('id' in submitter) { + return { + submitterAccount: submitter.id as KiltAddress, + submitterSigner: submitter, + } + } + // MultibaseKeyPair + if ('publicKeyMultibase' in submitter) { + const submitterAccount = Crypto.encodeAddress( + Multikey.decodeMultibaseKeypair(submitter).publicKey, + 38 + ) + const keypair = Multikey.decodeMultibaseKeypair(submitter) + if (keypair.type === 'x25519') { + throw new Error('x25519 keys are not supported') + } + const keyring = new Keyring().createFromPair( + keypair, + undefined, + keypair.type === 'secp256k1' ? 'ecdsa' : keypair.type + ) + return { + submitterAccount, + submitterSigner: keyring, + } + } + throw new Error('type of submitter is invalid') +} diff --git a/packages/sdk-js/src/DidHelpers/createDid.spec.ts b/packages/sdk-js/src/DidHelpers/createDid.spec.ts new file mode 100644 index 000000000..9055b9e32 --- /dev/null +++ b/packages/sdk-js/src/DidHelpers/createDid.spec.ts @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2018-2024, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import { resolver } from '@kiltprotocol/did' +import type { KiltKeyringPair, TransactionResult } from '@kiltprotocol/types' +import { Crypto } from '@kiltprotocol/utils' +import { SubmittableResult } from '@polkadot/api' +import { + ApiMocks, + createLocalDemoFullDidFromKeypair, +} from '../../../../tests/testUtils/index.js' +import { ConfigService } from '../index.js' +import { createDid } from './createDid.js' + +const mockedApi = ApiMocks.createAugmentedApi() +jest.mock('@kiltprotocol/did', () => { + return { + ...jest.requireActual('@kiltprotocol/did'), + resolver: { resolve: jest.fn() }, + authorizeTx: jest.fn(), + } +}) + +describe('createDid', () => { + let keypair: KiltKeyringPair + beforeAll(async () => { + ConfigService.set({ api: mockedApi }) + + keypair = Crypto.makeKeypairFromUri('//Alice') + const didDocument = await createLocalDemoFullDidFromKeypair(keypair) + jest.mocked(resolver).resolve.mockImplementation((did) => { + if (did === didDocument.id) { + return { didDocument } as any + } + throw new Error() + }) + }) + + it('creates create did tx', async () => { + const { txHex, checkResult } = await createDid({ + api: mockedApi, + submitter: keypair, + signers: [keypair], + fromPublicKey: keypair, + }).getSubmittable({ signSubmittable: false }) + + expect(txHex).toContain('0x') + const parsed = mockedApi.tx(txHex) + + const result = await checkResult( + new SubmittableResult({ + blockNumber: mockedApi.createType('BlockNumber', 1000), + status: mockedApi.createType('ExtrinsicStatus', { + inBlock: new Uint8Array(32).fill(2), + }), + txHash: parsed.hash, + events: [ + { + event: { + method: 'didCreated', + section: 'did', + }, + } as any, + ], + }) + ) + + expect(result).toMatchObject>({ + status: 'confirmed', + asConfirmed: expect.objectContaining({ + txHash: parsed.hash.toHex(), + block: { + hash: mockedApi + .createType('Hash', new Uint8Array(32).fill(2)) + .toHex(), + number: 1000n, + }, + }), + }) + }) +}) diff --git a/packages/sdk-js/src/DidHelpers/createDid.ts b/packages/sdk-js/src/DidHelpers/createDid.ts new file mode 100644 index 000000000..ab4f97b12 --- /dev/null +++ b/packages/sdk-js/src/DidHelpers/createDid.ts @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2018-2024, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import { Blockchain } from '@kiltprotocol/chain-helpers' +import { + getFullDid, + getStoreTx, + type NewDidVerificationKey, + signingMethodTypes, +} from '@kiltprotocol/did' +import type { + DidHelpersAcceptedPublicKeyEncodings, + SharedArguments, + SignerInterface, + TransactionHandlers, +} from '@kiltprotocol/types' +import { Crypto, type Multikey, Signers } from '@kiltprotocol/utils' +import { checkResultImpl } from './checkResult.js' + +import { + convertPublicKey, + extractSubmitterSignerAndAccount, + submitImpl, +} from './common.js' + +function implementsSignerInterface(input: any): input is SignerInterface { + return 'algorithm' in input && 'id' in input && 'sign' in input +} + +/** + * Creates an on-chain DID based on an authentication key. + * + * @param options Any {@link SharedArguments} (minus `didDocument`) and additional parameters. + * @param options.fromPublicKey The public key that will feature as the DID's initial authentication method and will determine the DID identifier. + * @returns A set of {@link TransactionHandlers}. + */ +export function createDid( + options: Omit & { + fromPublicKey: DidHelpersAcceptedPublicKeyEncodings + } +): TransactionHandlers { + const getSubmittable: TransactionHandlers['getSubmittable'] = async ( + submitOptions = {} + ) => { + const { fromPublicKey, submitter, signers, api } = options + const { signSubmittable = false } = submitOptions + const { publicKey, type } = convertPublicKey(fromPublicKey) + + if (!signingMethodTypes.includes(type)) { + throw new Error(`unknown key type ${type}`) + } + const { submitterSigner, submitterAccount } = + extractSubmitterSignerAndAccount(submitter) + + const accountSigners = ( + await Promise.all( + signers.map(async (signer) => { + if (implementsSignerInterface(signer)) { + return [signer] + } + const res = await Signers.getSignersForKeypair({ + keypair: signer, + }) + return res + }) + ) + ).flat() + + let didCreation = await getStoreTx( + { + authentication: [{ publicKey, type } as NewDidVerificationKey], + }, + submitterAccount, + accountSigners + ) + + if (signSubmittable) { + if (typeof submitterSigner === 'undefined') { + throw new Error('submitter does not include a secret key') + } + didCreation = await Blockchain.signTx(didCreation, submitterSigner) + } + + return { + txHex: didCreation.toHex(), + checkResult: (input) => + checkResultImpl( + input, + api, + [{ section: 'did', method: 'DidCreated' }], + getFullDid(Crypto.encodeAddress(publicKey, 38)), + signers + ), + } + } + + const submit: TransactionHandlers['submit'] = (submitOptions) => + submitImpl(getSubmittable, { ...options, ...submitOptions }) + + return { getSubmittable, submit } +} diff --git a/packages/sdk-js/src/DidHelpers/deactivateDid.spec.ts b/packages/sdk-js/src/DidHelpers/deactivateDid.spec.ts new file mode 100644 index 000000000..c42c395a8 --- /dev/null +++ b/packages/sdk-js/src/DidHelpers/deactivateDid.spec.ts @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2018-2024, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import type { DidDocument, KiltKeyringPair } from '@kiltprotocol/types' +import { Crypto } from '@kiltprotocol/utils' +import { + ApiMocks, + createLocalDemoFullDidFromKeypair, +} from '../../../../tests/testUtils/index.js' +import { ConfigService } from '../index.js' +import { deactivateDid } from './deactivateDid.js' +import { transactInternal } from './transact.js' + +jest.mock('./transact.js') + +const mockedTransact = jest.mocked(transactInternal) +const mockedApi = ApiMocks.createAugmentedApi() + +describe('deactivate', () => { + let didDocument: DidDocument + let keypair: KiltKeyringPair + beforeAll(async () => { + ConfigService.set({ api: mockedApi }) + + keypair = Crypto.makeKeypairFromUri('//Alice') + const { id, verificationMethod, authentication } = + await createLocalDemoFullDidFromKeypair(keypair, { + verificationRelationships: new Set(['assertionMethod']), + }) + didDocument = { + id, + authentication, + assertionMethod: authentication, + verificationMethod: verificationMethod?.filter( + (vm) => vm.id === authentication![0] + ), + } + }) + + it('creates a deactivate did tx', async () => { + deactivateDid({ + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + }) + + expect(mockedTransact).toHaveBeenLastCalledWith( + expect.objectContaining[0]>>({ + callFactory: expect.any(Function), + expectedEvents: expect.arrayContaining([ + { + section: 'did', + method: 'DidDeleted', + }, + ]), + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + }) + ) + expect( + await mockedTransact.mock.lastCall?.[0] + .callFactory() + .then((f) => f.toHuman()) + ).toMatchObject({ + method: { method: 'delete', section: 'did' }, + }) + }) +}) diff --git a/packages/sdk-js/src/DidHelpers/deactivateDid.ts b/packages/sdk-js/src/DidHelpers/deactivateDid.ts new file mode 100644 index 000000000..a35fb0546 --- /dev/null +++ b/packages/sdk-js/src/DidHelpers/deactivateDid.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2018-2024, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import type { SharedArguments, TransactionHandlers } from '@kiltprotocol/types' +import { transactInternal } from './transact.js' + +/** + * _Permanently_ deactivates the DID, removing all verification methods and services from its document. + * This action cannot be undone – once a DID has been deactivated, all operations on it (including attempts at re-creation) are permanently disabled. + * + * @param options Any {@link SharedArguments} and additional parameters. + * @param options.didDocument The {@link DidDocument} of the DID to be deactivated. + * @returns A set of {@link TransactionHandlers}. + */ +export function deactivateDid(options: SharedArguments): TransactionHandlers { + const { api, didDocument } = options + return transactInternal({ + ...options, + callFactory: async () => + api.tx.did.delete(didDocument.service?.length ?? 0), + expectedEvents: [{ section: 'did', method: 'DidDeleted' }], + }) +} diff --git a/packages/sdk-js/src/DidHelpers/index.ts b/packages/sdk-js/src/DidHelpers/index.ts new file mode 100644 index 000000000..876806254 --- /dev/null +++ b/packages/sdk-js/src/DidHelpers/index.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2018-2024, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +export { createDid } from './createDid.js' +export { deactivateDid } from './deactivateDid.js' +export { selectSigner } from './selectSigner.js' +export { addService, removeService } from './service.js' +export { transact } from './transact.js' +export { + removeVerificationMethod, + setVerificationMethod, +} from './verificationMethod.js' +export { claimWeb3Name, releaseWeb3Name } from './w3names.js' diff --git a/packages/sdk-js/src/DidHelpers/selectSigner.ts b/packages/sdk-js/src/DidHelpers/selectSigner.ts new file mode 100644 index 000000000..2cf7efe44 --- /dev/null +++ b/packages/sdk-js/src/DidHelpers/selectSigner.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2018-2024, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import { signersForDid } from '@kiltprotocol/did' +import type { + DidUrl, + SignerInterface, + SharedArguments, +} from '@kiltprotocol/types' +import { Signers } from '@kiltprotocol/utils' + +/** + * Selects and returns a DID signer for a given purpose and algorithm. + * + * @param options All options. + * @param options.signers Signers from which to choose from; can also be `KeyringPair` instances or other key pair representations. + * @param options.relationship Which verification relationship the key should have to the DID. + * Defaults to `authentication`. + * @param options.algorithm Optionally filter signers by algorithm(s). + * @param options.didDocument The DID's DID document. + */ +export async function selectSigner({ + didDocument, + signers, + relationship = 'authentication', + algorithm, +}: Pick & { + relationship?: string + algorithm?: string | string[] +}): Promise | undefined> { + const mappedSigners = await signersForDid(didDocument, ...signers) + const selectors = [ + Signers.select.byDid(didDocument, { + verificationRelationship: relationship, + }), + ] + if (typeof algorithm !== 'undefined') { + Signers.select.byAlgorithm( + Array.isArray(algorithm) ? algorithm : [algorithm] + ) + } + + return Signers.selectSigner(mappedSigners, ...selectors) +} diff --git a/packages/sdk-js/src/DidHelpers/service.spec.ts b/packages/sdk-js/src/DidHelpers/service.spec.ts new file mode 100644 index 000000000..b2d79712a --- /dev/null +++ b/packages/sdk-js/src/DidHelpers/service.spec.ts @@ -0,0 +1,136 @@ +/** + * Copyright (c) 2018-2024, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import type { DidDocument, DidUrl, KiltKeyringPair } from '@kiltprotocol/types' +import { Crypto } from '@kiltprotocol/utils' +import { + ApiMocks, + createLocalDemoFullDidFromKeypair, +} from '../../../../tests/testUtils/index.js' +import { ConfigService } from '../index.js' +import { addService, removeService } from './service.js' +import { transactInternal } from './transact.js' + +jest.mock('./transact.js') + +const mockedTransact = jest.mocked(transactInternal) +const mockedApi = ApiMocks.createAugmentedApi() + +describe.each(['#my_service', 'did:kilt:4abctest#my_service'])( + 'service management with id %s', + (serviceId) => { + let didDocument: DidDocument + let keypair: KiltKeyringPair + beforeAll(async () => { + ConfigService.set({ api: mockedApi }) + + keypair = Crypto.makeKeypairFromUri('//Alice') + const { id, verificationMethod, authentication } = + await createLocalDemoFullDidFromKeypair(keypair, { + verificationRelationships: new Set(['assertionMethod']), + }) + didDocument = { + id, + authentication, + assertionMethod: authentication, + verificationMethod: verificationMethod?.filter( + (vm) => vm.id === authentication![0] + ), + } + }) + + it('creates an add service tx', async () => { + addService({ + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + service: { + id: serviceId as DidUrl, + type: ['http://schema.org/EmailService'], + serviceEndpoint: ['mailto:info@kilt.io'], + }, + }) + + expect(mockedTransact).toHaveBeenLastCalledWith( + expect.objectContaining< + Partial[0]> + >({ + callFactory: expect.any(Function), + expectedEvents: expect.arrayContaining([ + { + section: 'did', + method: 'DidUpdated', + }, + ]), + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + }) + ) + expect( + await mockedTransact.mock.lastCall?.[0] + .callFactory() + .then((f) => f.toHuman()) + ).toMatchObject({ + method: { + args: { + service_endpoint: { + id: 'my_service', + serviceTypes: ['http://schema.org/EmailService'], + urls: ['mailto:info@kilt.io'], + }, + }, + section: 'did', + method: 'addServiceEndpoint', + }, + }) + }) + + it('creates a remove service tx', async () => { + removeService({ + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + id: serviceId as DidUrl, + }) + + expect(mockedTransact).toHaveBeenLastCalledWith( + expect.objectContaining< + Partial[0]> + >({ + callFactory: expect.any(Function), + expectedEvents: expect.arrayContaining([ + { + section: 'did', + method: 'DidUpdated', + }, + ]), + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + }) + ) + expect( + await mockedTransact.mock.lastCall?.[0] + .callFactory() + .then((f) => f.toHuman()) + ).toMatchObject({ + method: { + args: { + service_id: 'my_service', + }, + section: 'did', + method: 'removeServiceEndpoint', + }, + }) + }) + } +) diff --git a/packages/sdk-js/src/DidHelpers/service.ts b/packages/sdk-js/src/DidHelpers/service.ts new file mode 100644 index 000000000..95b07563e --- /dev/null +++ b/packages/sdk-js/src/DidHelpers/service.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2018-2024, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import { serviceToChain, urlFragmentToChain } from '@kiltprotocol/did' +import type { + DidUrl, + Service, + SharedArguments, + TransactionHandlers, + UriFragment, +} from '@kiltprotocol/types' +import { transactInternal } from './transact.js' + +/** + * Adds a service to the DID Document. + * + * @param options Any {@link SharedArguments} and additional parameters. + * @param options.service The service entry to add to the document. + * If the service id is relative (begins with #) it is automatically expanded with the DID taken from didDocument.id. + * @returns A set of {@link TransactionHandlers}. + */ +export function addService( + options: SharedArguments & { + service: Service + } +): TransactionHandlers { + return transactInternal({ + ...options, + callFactory: async () => + options.api.tx.did.addServiceEndpoint(serviceToChain(options.service)), + expectedEvents: [{ section: 'did', method: 'DidUpdated' }], + }) +} + +/** + * Removes a service from the DID Document. + * + * @param options Any {@link SharedArguments} and additional parameters. + * @param options.id The id of the service to remove from the document. + * If the service id is relative (begins with #) it is automatically expanded with the DID taken from didDocument.id. + * @returns A set of {@link TransactionHandlers}. + */ +export function removeService( + options: SharedArguments & { + id: DidUrl | UriFragment + } +): TransactionHandlers { + return transactInternal({ + ...options, + callFactory: async () => + options.api.tx.did.removeServiceEndpoint(urlFragmentToChain(options.id)), + expectedEvents: [{ section: 'did', method: 'DidUpdated' }], + }) +} diff --git a/packages/sdk-js/src/DidHelpers/transact.spec.ts b/packages/sdk-js/src/DidHelpers/transact.spec.ts new file mode 100644 index 000000000..3b919d78a --- /dev/null +++ b/packages/sdk-js/src/DidHelpers/transact.spec.ts @@ -0,0 +1,143 @@ +/** + * Copyright (c) 2018-2024, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import { authorizeTx, resolver } from '@kiltprotocol/did' +import type { + DidDocument, + KiltKeyringPair, + TransactionResult, +} from '@kiltprotocol/types' +import { Crypto } from '@kiltprotocol/utils' +import { SubmittableResult } from '@polkadot/api' + +import { + ApiMocks, + createLocalDemoFullDidFromKeypair, +} from '../../../../tests/testUtils/index.js' +import { makeAttestationCreatedEvents } from '../../../../tests/testUtils/testData.js' +import { ConfigService } from '../index.js' +import { transact } from './transact.js' + +jest.mock('@kiltprotocol/did', () => { + return { + ...jest.requireActual('@kiltprotocol/did'), + resolver: { resolve: jest.fn() }, + authorizeTx: jest.fn(), + } +}) + +const mockedApi = ApiMocks.createAugmentedApi() + +describe('transact', () => { + let didDocument: DidDocument + let keypair: KiltKeyringPair + beforeAll(async () => { + ConfigService.set({ api: mockedApi }) + + keypair = Crypto.makeKeypairFromUri('//Alice') + didDocument = await createLocalDemoFullDidFromKeypair(keypair) + jest + .mocked(authorizeTx) + .mockImplementation(async (_did, ex, _sig, sub, { txCounter } = {}) => { + return mockedApi.tx.did.submitDidCall( + { + did: keypair.address, + txCounter: txCounter ?? 0, + call: ex, + blockNumber: 0, + submitter: sub, + }, + { ed25519: new Uint8Array(64) } + ) + }) + jest.mocked(resolver).resolve.mockImplementation((did) => { + if (did === didDocument.id) { + return { didDocument } as any + } + throw new Error() + }) + }, 40000) + + it('creates a tx and checks status', async () => { + const { txHex, checkResult } = await transact({ + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + call: mockedApi.tx.attestation.add( + new Uint8Array(32).fill(1), + new Uint8Array(32).fill(1), + null + ), + expectedEvents: [ + { section: 'attestation', method: 'AttestationCreated' }, + ], + }).getSubmittable() + + expect(txHex).toContain('0x') + const parsed = mockedApi.tx(txHex) + expect(parsed.method).toHaveProperty('section', 'did') + expect(parsed.method).toHaveProperty('method', 'submitDidCall') + + const result = await checkResult( + new SubmittableResult({ + blockNumber: mockedApi.createType('BlockNumber', 1000), + status: mockedApi.createType('ExtrinsicStatus', { + inBlock: new Uint8Array(32).fill(2), + }), + txHash: parsed.hash, + events: makeAttestationCreatedEvents([[]]), + }) + ) + expect(result).toMatchObject>({ + status: 'confirmed', + asConfirmed: expect.objectContaining({ + txHash: parsed.hash.toHex(), + block: { + hash: mockedApi + .createType('Hash', new Uint8Array(32).fill(2)) + .toHex(), + number: 1000n, + }, + }), + }) + // TODO move serialzation test to `checkResult.spec.ts` once created and + // test the `asFailed` case. + const confirmed = result.asConfirmed + expect(typeof confirmed.block.number).toBe('bigint') + const resultStringified = JSON.stringify(confirmed) + const resultRebuiltObj = JSON.parse(resultStringified) + expect(BigInt(resultRebuiltObj.block.number)).toBe(BigInt(1000)) + expect(typeof confirmed.block.number).toBe('bigint') + expect(result.toJSON()).toStrictEqual({ + status: 'confirmed', + value: confirmed.toJSON(), + }) + }) + + it('creates an unsigned submittable', async () => { + const { txHex } = await transact({ + didDocument, + api: mockedApi, + submitter: keypair.address, + signers: [keypair], + call: mockedApi.tx.attestation.add( + new Uint8Array(32).fill(1), + new Uint8Array(32).fill(1), + null + ), + expectedEvents: [ + { section: 'attestation', method: 'AttestationCreated' }, + ], + }).getSubmittable({ signSubmittable: false }) + + expect(txHex).toContain('0x') + const parsed = mockedApi.tx(txHex) + expect(parsed.method).toHaveProperty('section', 'did') + expect(parsed.method).toHaveProperty('method', 'submitDidCall') + }) +}) diff --git a/packages/sdk-js/src/DidHelpers/transact.ts b/packages/sdk-js/src/DidHelpers/transact.ts new file mode 100644 index 000000000..d8b1f33b8 --- /dev/null +++ b/packages/sdk-js/src/DidHelpers/transact.ts @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2018-2024, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import type { Extrinsic } from '@polkadot/types/interfaces' +import { Blockchain } from '@kiltprotocol/chain-helpers' +import { authorizeTx, signersForDid } from '@kiltprotocol/did' +import type { + SubmittableExtrinsic, + SharedArguments, + TransactionHandlers, +} from '@kiltprotocol/types' +import { extractSubmitterSignerAndAccount, submitImpl } from './common.js' +import { checkResultImpl } from './checkResult.js' + +/** + * Instructs a transaction (state transition) as this DID (with this DID as the origin). + * + * @param options Any {@link SharedArguments} and additional parameters. + * @param options.callFactory Async callback producing the transaction / call to execute. + * @returns A set of {@link TransactionHandlers}. + */ +export function transactInternal( + options: SharedArguments & { + callFactory: () => Promise + expectedEvents?: Array<{ section: string; method: string }> + } +): TransactionHandlers { + const getSubmittable: TransactionHandlers['getSubmittable'] = async ( + submitOptions: + | { + signSubmittable?: boolean // default: false + didNonce?: number | BigInt + } + | undefined = {} + ) => { + const { + didDocument, + signers, + submitter, + callFactory, + api, + expectedEvents, + } = options + const { didNonce, signSubmittable = false } = submitOptions + const call = await callFactory() + const didSigners = await signersForDid(didDocument, ...signers) + + const { submitterSigner, submitterAccount } = + extractSubmitterSignerAndAccount(submitter) + + let authorized: SubmittableExtrinsic = await authorizeTx( + didDocument, + call, + didSigners, + submitterAccount, + typeof didNonce !== 'undefined' + ? { + txCounter: api.createType('u64', didNonce), + } + : {} + ) + + if (signSubmittable) { + if (typeof submitterSigner === 'undefined') { + throw new Error('submitter does not include a secret key') + } + authorized = await Blockchain.signTx(authorized, submitterSigner) + } + + return { + txHex: authorized.toHex(), + checkResult: (input) => + checkResultImpl(input, api, expectedEvents, didDocument.id, signers), + } + } + + const submit: TransactionHandlers['submit'] = (submitOptions) => + submitImpl(getSubmittable, { ...options, ...submitOptions }) + + return { + submit, + getSubmittable, + } +} + +/** + * Instructs a transaction (state transition) as this DID (with this DID as the origin). + * + * @param options Any {@link SharedArguments} and additional parameters. + * @param options.call The transaction / call to execute. + * @returns A set of {@link TransactionHandlers}. + */ +export function transact( + options: SharedArguments & { + call: Extrinsic | SubmittableExtrinsic + expectedEvents?: Array<{ section: string; method: string }> + } +): TransactionHandlers { + return transactInternal({ ...options, callFactory: async () => options.call }) +} diff --git a/packages/sdk-js/src/DidHelpers/verificationMethod.spec.ts b/packages/sdk-js/src/DidHelpers/verificationMethod.spec.ts new file mode 100644 index 000000000..9e468aa24 --- /dev/null +++ b/packages/sdk-js/src/DidHelpers/verificationMethod.spec.ts @@ -0,0 +1,261 @@ +/** + * Copyright (c) 2018-2024, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import type { DidDocument, KiltKeyringPair } from '@kiltprotocol/types' +import { Crypto } from '@kiltprotocol/utils' +import { + ApiMocks, + createLocalDemoFullDidFromKeypair, +} from '../../../../tests/testUtils/index.js' +import { ConfigService } from '../index.js' +import { transactInternal } from './transact.js' +import { + removeVerificationMethod, + setVerificationMethod, +} from './verificationMethod.js' + +jest.mock('./transact.js') + +const mockedTransact = jest.mocked(transactInternal) +const mockedApi = ApiMocks.createAugmentedApi() + +let didDocument: DidDocument +let keypair: KiltKeyringPair +beforeAll(async () => { + ConfigService.set({ api: mockedApi }) + + keypair = Crypto.makeKeypairFromUri('//Alice') + const { id, verificationMethod, authentication } = + await createLocalDemoFullDidFromKeypair(keypair, { + verificationRelationships: new Set(['assertionMethod']), + }) + didDocument = { + id, + authentication, + assertionMethod: authentication, + verificationMethod: verificationMethod?.filter( + (vm) => vm.id === authentication![0] + ), + } +}) + +describe('signing keys', () => { + it('creates a set VM tx', async () => { + setVerificationMethod({ + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + publicKey: keypair, + relationship: 'assertionMethod', + }) + + expect(mockedTransact).toHaveBeenLastCalledWith( + expect.objectContaining[0]>>({ + callFactory: expect.any(Function), + expectedEvents: expect.arrayContaining([ + { + section: 'did', + method: 'DidUpdated', + }, + ]), + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + }) + ) + expect( + await mockedTransact.mock.lastCall?.[0] + .callFactory() + .then((f) => f.toHuman()) + ).toMatchObject({ + method: { + section: 'did', + method: 'setAttestationKey', + args: { new_key: { Ed25519: Crypto.u8aToHex(keypair.publicKey) } }, + }, + }) + }) + + it('creates a remove VM tx', async () => { + didDocument.assertionMethod = didDocument.authentication + removeVerificationMethod({ + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + verificationMethodId: didDocument.assertionMethod![0], + relationship: 'assertionMethod', + }) + + expect(mockedTransact).toHaveBeenLastCalledWith( + expect.objectContaining[0]>>({ + callFactory: expect.any(Function), + expectedEvents: expect.arrayContaining([ + { + section: 'did', + method: 'DidUpdated', + }, + ]), + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + }) + ) + expect( + await mockedTransact.mock.lastCall?.[0] + .callFactory() + .then((f) => f.toHuman()) + ).toMatchObject({ + method: { + section: 'did', + method: 'removeAttestationKey', + }, + }) + }) +}) + +describe('key agreement keys', () => { + it('creates a set VM tx for the first key agreement', async () => { + setVerificationMethod({ + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + publicKey: { publicKey: keypair.publicKey, type: 'x25519' }, + relationship: 'keyAgreement', + }) + + expect(mockedTransact).toHaveBeenLastCalledWith( + expect.objectContaining[0]>>({ + callFactory: expect.any(Function), + expectedEvents: expect.arrayContaining([ + { + section: 'did', + method: 'DidUpdated', + }, + ]), + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + }) + ) + expect( + await mockedTransact.mock.lastCall?.[0] + .callFactory() + .then((f) => f.toHuman()) + ).toMatchObject({ + method: { + section: 'utility', + method: 'batchAll', + args: { + calls: [ + { + section: 'did', + method: 'addKeyAgreementKey', + args: { new_key: { X25519: Crypto.u8aToHex(keypair.publicKey) } }, + }, + ], + }, + }, + }) + }) + + it('creates a set VM tx for the second key agreement', async () => { + didDocument.keyAgreement = [ + `${didDocument.id}#${Crypto.hashStr('keyAgreement1')}`, + ] + setVerificationMethod({ + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + publicKey: { publicKey: keypair.publicKey, type: 'x25519' }, + relationship: 'keyAgreement', + }) + + expect(mockedTransact).toHaveBeenLastCalledWith( + expect.objectContaining[0]>>({ + callFactory: expect.any(Function), + expectedEvents: expect.arrayContaining([ + { + section: 'did', + method: 'DidUpdated', + }, + ]), + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + }) + ) + expect( + await mockedTransact.mock.lastCall?.[0] + .callFactory() + .then((f) => f.toHuman()) + ).toMatchObject({ + method: { + section: 'utility', + method: 'batchAll', + args: { + calls: [ + { + section: 'did', + method: 'removeKeyAgreementKey', + args: { key_id: expect.stringContaining('0x') }, + }, + { + section: 'did', + method: 'addKeyAgreementKey', + args: { new_key: { X25519: Crypto.u8aToHex(keypair.publicKey) } }, + }, + ], + }, + }, + }) + }) + + it('creates a remove VM tx', async () => { + removeVerificationMethod({ + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + verificationMethodId: didDocument.keyAgreement![0], + relationship: 'keyAgreement', + }) + + expect(mockedTransact).toHaveBeenLastCalledWith( + expect.objectContaining[0]>>({ + callFactory: expect.any(Function), + expectedEvents: expect.arrayContaining([ + { + section: 'did', + method: 'DidUpdated', + }, + ]), + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + }) + ) + expect( + await mockedTransact.mock.lastCall?.[0] + .callFactory() + .then((f) => f.toHuman()) + ).toMatchObject({ + method: { + section: 'did', + method: 'removeKeyAgreementKey', + }, + }) + }) +}) diff --git a/packages/sdk-js/src/DidHelpers/verificationMethod.ts b/packages/sdk-js/src/DidHelpers/verificationMethod.ts new file mode 100644 index 000000000..536ed3d09 --- /dev/null +++ b/packages/sdk-js/src/DidHelpers/verificationMethod.ts @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2018-2024, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import { + type NewDidEncryptionKey, + type NewDidVerificationKey, + publicKeyToChain, + urlFragmentToChain, +} from '@kiltprotocol/did' +import type { + DidHelpersAcceptedPublicKeyEncodings, + DidUrl, + SharedArguments, + SubmittableExtrinsic, + TransactionHandlers, + VerificationRelationship, +} from '@kiltprotocol/types' +import type { Multikey } from '@kiltprotocol/utils' + +import { convertPublicKey } from './common.js' +import { transactInternal } from './transact.js' + +/** + * Replaces all existing verification methods for the selected `relationship` with `publicKey`. + * + * @param options Any {@link SharedArguments} and additional parameters. + * @param options.publicKey The public key to be used for this verification method. + * @param options.relationship The relationship for which this verification method shall be useable. + * + * @returns A set of {@link TransactionHandlers}. + */ +export function setVerificationMethod( + options: SharedArguments & { + publicKey: DidHelpersAcceptedPublicKeyEncodings + relationship: VerificationRelationship + } +): TransactionHandlers { + const callFactory = async (): Promise => { + const { publicKey, relationship, didDocument, api } = options + const pk = convertPublicKey(publicKey) + + switch (relationship) { + case 'keyAgreement': { + const txs: SubmittableExtrinsic[] = [] + didDocument.keyAgreement?.forEach((id) => + txs.push(api.tx.did.removeKeyAgreementKey(urlFragmentToChain(id))) + ) + txs.push( + api.tx.did.addKeyAgreementKey( + publicKeyToChain(pk as NewDidEncryptionKey) + ) + ) + return api.tx.utility.batchAll(txs) + } + case 'authentication': { + return api.tx.did.setAuthenticationKey( + publicKeyToChain(pk as NewDidVerificationKey) + ) + } + case 'capabilityDelegation': { + return api.tx.did.setDelegationKey( + publicKeyToChain(pk as NewDidVerificationKey) + ) + } + case 'assertionMethod': { + return api.tx.did.setAttestationKey( + publicKeyToChain(pk as NewDidVerificationKey) + ) + } + default: { + throw new Error('unsupported relationship') + } + } + } + + return transactInternal({ + ...options, + callFactory, + expectedEvents: [{ section: 'did', method: 'DidUpdated' }], + }) +} + +/** + * Removes the verification method for the selected `verificationMethodId` and `relationship`. + * + * Note: authentication verification method can not be removed. + * + * @param options Any {@link SharedArguments} and additional parameters. + * @param options.relationship The relationship for which this verification method shall be removed. + * @param options.verificationMethodId The id of the verification method that should be removed. + * + * @returns A set of {@link TransactionHandlers}. + */ +export function removeVerificationMethod( + options: SharedArguments & { + verificationMethodId: DidUrl + relationship: Omit + } +): TransactionHandlers { + const callFactory = async (): Promise => { + const { relationship, didDocument, api, verificationMethodId } = options + switch (relationship) { + case 'authentication': { + throw new Error( + 'authentication verification methods can not be removed' + ) + } + case 'capabilityDelegation': { + if (didDocument.capabilityDelegation?.includes(verificationMethodId)) { + return api.tx.did.removeDelegationKey() + } + throw new Error( + 'the specified capabilityDelegation method does not exist in the DID Document' + ) + } + case 'keyAgreement': { + if (didDocument.keyAgreement?.includes(verificationMethodId)) { + return api.tx.did.removeKeyAgreementKey( + urlFragmentToChain(verificationMethodId) + ) + } + throw new Error( + 'the specified keyAgreement key does not exist in the DID Document' + ) + } + case 'assertionMethod': { + if (didDocument.assertionMethod?.includes(verificationMethodId)) { + return api.tx.did.removeAttestationKey() + } + throw new Error( + 'the specified assertionMethod does not exist in the DID Document' + ) + } + default: { + throw new Error('the specified method relationship is not supported') + } + } + } + + return transactInternal({ + ...options, + callFactory, + expectedEvents: [{ section: 'did', method: 'DidUpdated' }], + }) +} diff --git a/packages/sdk-js/src/DidHelpers/w3names.spec.ts b/packages/sdk-js/src/DidHelpers/w3names.spec.ts new file mode 100644 index 000000000..55f625e51 --- /dev/null +++ b/packages/sdk-js/src/DidHelpers/w3names.spec.ts @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2018-2024, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import type { DidDocument, KiltKeyringPair } from '@kiltprotocol/types' +import { Crypto } from '@kiltprotocol/utils' +import { + ApiMocks, + createLocalDemoFullDidFromKeypair, +} from '../../../../tests/testUtils/index.js' +import { ConfigService } from '../index.js' +import { transactInternal } from './transact.js' +import { claimWeb3Name, releaseWeb3Name } from './w3names.js' + +jest.mock('./transact.js') + +const mockedTransact = jest.mocked(transactInternal) +const mockedApi = ApiMocks.createAugmentedApi() + +describe('w3n', () => { + let didDocument: DidDocument + let keypair: KiltKeyringPair + beforeAll(async () => { + ConfigService.set({ api: mockedApi }) + + keypair = Crypto.makeKeypairFromUri('//Alice') + const { id, verificationMethod, authentication } = + await createLocalDemoFullDidFromKeypair(keypair, { + verificationRelationships: new Set(['assertionMethod']), + }) + didDocument = { + id, + authentication, + assertionMethod: authentication, + verificationMethod: verificationMethod?.filter( + (vm) => vm.id === authentication![0] + ), + } + }) + + it('creates a claim w3n tx', async () => { + claimWeb3Name({ + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + name: 'paul', + }) + + expect(mockedTransact).toHaveBeenLastCalledWith( + expect.objectContaining[0]>>({ + callFactory: expect.any(Function), + expectedEvents: expect.arrayContaining([ + { + section: 'web3Names', + method: 'Web3NameClaimed', + }, + ]), + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + }) + ) + expect( + await mockedTransact.mock.lastCall?.[0] + .callFactory() + .then((f) => f.toHuman()) + ).toMatchObject({ + method: { args: { name: 'paul' }, method: 'claim', section: 'web3Names' }, + }) + }) + + it('creates a release w3n tx', async () => { + releaseWeb3Name({ + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + }) + + expect(mockedTransact).toHaveBeenLastCalledWith( + expect.objectContaining[0]>>({ + callFactory: expect.any(Function), + expectedEvents: expect.arrayContaining([ + { + section: 'web3Names', + method: 'Web3NameReleased', + }, + ]), + didDocument, + api: mockedApi, + submitter: keypair, + signers: [keypair], + }) + ) + expect( + await mockedTransact.mock.lastCall?.[0] + .callFactory() + .then((f) => f.toHuman()) + ).toMatchObject({ + method: { method: 'releaseByOwner', section: 'web3Names' }, + }) + }) +}) diff --git a/packages/sdk-js/src/DidHelpers/w3names.ts b/packages/sdk-js/src/DidHelpers/w3names.ts new file mode 100644 index 000000000..fee5a416c --- /dev/null +++ b/packages/sdk-js/src/DidHelpers/w3names.ts @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2018-2024, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import type { SharedArguments, TransactionHandlers } from '@kiltprotocol/types' +import { transactInternal } from './transact.js' + +/** + * Adds a w3n nickname to the DID Document. + * + * @param options Any {@link SharedArguments} and additional parameters. + * @param options.name The name to be claimed. + * Must be still available (not yet claimed by another DID) for this operation to succeed. + * @returns A set of {@link TransactionHandlers}. + */ +export function claimWeb3Name( + options: SharedArguments & { + name: string + } +): TransactionHandlers { + const { api, name } = options + return transactInternal({ + ...options, + callFactory: async () => api.tx.web3Names.claim(name), + expectedEvents: [{ section: 'web3Names', method: 'Web3NameClaimed' }], + }) +} + +/** + * Removes w3n nickname from the DID Document, allowing it to be claimed by others. + * + * @param options Any {@link SharedArguments} and additional parameters. + * @returns A set of {@link TransactionHandlers}. + */ +export function releaseWeb3Name(options: SharedArguments): TransactionHandlers { + const { api } = options + return transactInternal({ + ...options, + callFactory: async () => api.tx.web3Names.releaseByOwner(), + expectedEvents: [{ section: 'web3Names', method: 'Web3NameReleased' }], + }) +} diff --git a/packages/sdk-js/src/index.ts b/packages/sdk-js/src/index.ts index c1ef1bff2..485126fb4 100644 --- a/packages/sdk-js/src/index.ts +++ b/packages/sdk-js/src/index.ts @@ -19,9 +19,10 @@ import { Blockchain, } from '@kiltprotocol/chain-helpers' import { resolver as DidResolver } from '@kiltprotocol/did' +import * as DidHelpers from './DidHelpers/index.js' const { signAndSubmitTx } = Blockchain // TODO: maybe we don't even need that if we have the identity class -const { signerFromKeypair } = Signers +const { getSignersForKeypair, generateKeypair } = Signers export { init, @@ -31,7 +32,9 @@ export { Holder, Verifier, Issuer, - signerFromKeypair, + getSignersForKeypair, + generateKeypair, signAndSubmitTx, ConfigService, + DidHelpers, } diff --git a/packages/sdk-js/tsconfig.build.json b/packages/sdk-js/tsconfig.build.json index c7b062afb..e54a0b1c2 100644 --- a/packages/sdk-js/tsconfig.build.json +++ b/packages/sdk-js/tsconfig.build.json @@ -8,5 +8,8 @@ "include": [ "src/**/*.ts", "src/**/*.js" + ], + "exclude": [ + "**/*.spec.ts", ] } diff --git a/packages/types/package.json b/packages/types/package.json index c831b7b3a..71e4fe164 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@kiltprotocol/types", - "version": "0.100.0-alpha.1", + "version": "0.100.0-alpha.2", "description": "", "main": "./lib/cjs/index.js", "module": "./lib/esm/index.js", diff --git a/packages/types/src/Address.ts b/packages/types/src/Address.ts index 7bbd2b6b7..50a31f085 100644 --- a/packages/types/src/Address.ts +++ b/packages/types/src/Address.ts @@ -26,10 +26,10 @@ export type KiltAddress = KiltKeyringPair['address'] declare module '@polkadot/keyring' { function encodeAddress( key: HexString | Uint8Array | string, - ss58Format?: Prefix - ): string + ss58Format: 38 + ): KiltAddress function encodeAddress( key: HexString | Uint8Array | string, - ss58Format?: 38 - ): KiltAddress + ss58Format?: Prefix + ): string } diff --git a/packages/types/src/DidHelpers.ts b/packages/types/src/DidHelpers.ts new file mode 100644 index 000000000..27800f807 --- /dev/null +++ b/packages/types/src/DidHelpers.ts @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2018-2024, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import type { ApiPromise } from '@polkadot/api' +import type { SubmittableResultValue } from '@polkadot/api/types' +import type { GenericEvent } from '@polkadot/types' +import type { + MultibaseKeyPair, + MultibasePublicKey, + SignerInterface, + TransactionSigner, + TypedKeypair, +} from './Signers' +import { HexString, KeyringPair } from './Imported.js' +import { DidDocument } from './Did.js' +import { KiltAddress } from './Address.js' + +export interface TransactionResult { + status: 'confirmed' | 'failed' | 'rejected' | 'unknown' + // these are getters that would throw if status is not as expected + asConfirmed: { + txHash: HexString + signers: SignerInterface[] + didDocument: DidDocument + block: { hash: HexString; number: BigInt } + events: GenericEvent[] + toJSON: () => any + } + asFailed: { + error: Error + txHash: HexString + signers: SignerInterface[] + didDocument?: DidDocument + block: { hash: HexString; number: BigInt } + events: GenericEvent[] + toJSON: () => any + } + asRejected: { + error: Error + txHash: HexString + signers: SignerInterface[] + didDocument?: DidDocument + } + asUnknown: { + error: Error + txHash: HexString + } + toJSON: () => any +} + +export interface TransactionHandlers { + /** + * Submits a transaction for inclusion in a block, resulting in its execution in the blockchain runtime. + * + * @param options Options map to allow for named arguments. + * @param options.awaitFinalized If set to true, this waits for finalization of the block of inclusion before returning. + * @param options.didNonce Allows explicitly setting the nonce to be used in DID authorization. + * @returns A Promise resolving to the DID document and info on the success of the transaction. + */ + submit(options?: { + awaitFinalized?: boolean // default: false + didNonce?: number | BigInt + }): Promise + /** + * Produces a transaction that can be submitted to a blockchain node for inclusion, or signed and submitted by an external service. + * + * @param options Options map to allow for named arguments. + * @param options.signSubmittable If set to true, this signs the transaction with the submitterAccount, which covers transaction fees. + * In this case, if no signer is available for this account, the function throws. + * @param options.didNonce Allows explicitly setting the nonce to be used in DID authorization. + * @returns A Promise resolving to an Extrinsic object (encoded transaction). + */ + getSubmittable(options?: { + signSubmittable?: boolean // default: true + didNonce?: number | BigInt + }): Promise<{ + txHex: HexString + /** + * Takes info on the submission/inclusion of the transaction and evaluates whether it executed successfully. + * + * @param result A combination of the final transaction hash (which changes upon signing!) and the hash of the block in which the transaction was included; + * or constructor parameters for a {@link SubmittableResult}. + * @returns An object informing on the status and success of the transaction. + */ + checkResult( + result: + | { blockHash: HexString; txHash: HexString } + | SubmittableResultValue + ): Promise + }> +} + +export type DidHelpersAcceptedSigners = + | SignerInterface + | KeyringPair + | MultibaseKeyPair + +export type SharedArguments = { + didDocument: DidDocument + api: ApiPromise + signers: DidHelpersAcceptedSigners[] + submitter: KeyringPair | TransactionSigner | MultibaseKeyPair | KiltAddress +} + +type PublicKeyAndType = Pick< + TypedKeypair, + 'publicKey' | 'type' +> + +export type DidHelpersAcceptedPublicKeyEncodings = + | MultibasePublicKey['publicKeyMultibase'] + | MultibasePublicKey + | PublicKeyAndType // interface allows KeyringPair too diff --git a/packages/types/src/Imported.ts b/packages/types/src/Imported.ts index 1d46e3095..012d72d3e 100644 --- a/packages/types/src/Imported.ts +++ b/packages/types/src/Imported.ts @@ -15,3 +15,6 @@ export type { HexString } from '@polkadot/util/types' export type { Prefix } from '@polkadot/util-crypto/address/types' export type { SubmittableExtrinsic } from '@polkadot/api/promise/types' export type { KeyringPair } from '@polkadot/keyring/types' +export type { ApiPromise } from '@polkadot/api' +export type { SubmittableResultValue } from '@polkadot/api/types' +export type { GenericEvent } from '@polkadot/types' diff --git a/packages/types/src/Signers.ts b/packages/types/src/Signers.ts index cbfe18add..a6ceee32a 100644 --- a/packages/types/src/Signers.ts +++ b/packages/types/src/Signers.ts @@ -5,6 +5,9 @@ * found in the LICENSE file in the root directory of this source tree. */ +import type { KiltAddress } from './Address' +import type { Base58BtcMultibaseString, VerificationMethod } from './Did.js' + export type SignerInterface< Alg extends string = string, Id extends string = string @@ -13,3 +16,20 @@ export type SignerInterface< id: Id sign: (input: { data: Uint8Array }) => Promise } + +export type TransactionSigner = SignerInterface< + 'Ecrecover-Secp256k1-Blake2b' | 'Sr25519' | 'Ed25519', + KiltAddress +> + +export type MultibasePublicKey = Pick +export type MultibaseSecretKey = { + secretKeyMultibase: Base58BtcMultibaseString +} +export type MultibaseKeyPair = MultibasePublicKey & MultibaseSecretKey + +export type TypedKeypair = { + publicKey: Uint8Array + secretKey: Uint8Array + type: KeyTypes +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 446592fa1..190b35f81 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -26,3 +26,4 @@ export * from './DidResolver.js' export * from './PublicCredential.js' export * from './Imported.js' export * from './Signers.js' +export * from './DidHelpers.js' diff --git a/packages/utils/package.json b/packages/utils/package.json index dfa821e6a..c14e81f5e 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@kiltprotocol/utils", - "version": "0.100.0-alpha.1", + "version": "0.100.0-alpha.2", "description": "", "main": "./lib/cjs/index.js", "module": "./lib/esm/index.js", @@ -35,9 +35,10 @@ "typescript": "^4.8.3" }, "dependencies": { - "@kiltprotocol/eddsa-jcs-2022": "0.1.0-rc.3", - "@kiltprotocol/es256k-jcs-2023": "0.1.0-rc.3", - "@kiltprotocol/sr25519-jcs-2023": "0.1.0-rc.3", + "@kiltprotocol/eddsa-jcs-2022": "0.1.0-rc.4", + "@kiltprotocol/es256k-jcs-2023": "0.1.0-rc.4", + "@kiltprotocol/jcs-data-integrity-proofs-common": "0.1.0-rc.4", + "@kiltprotocol/sr25519-jcs-2023": "0.1.0-rc.4", "@kiltprotocol/types": "workspace:*", "@polkadot/api": "^12.0.0", "@polkadot/keyring": "^13.0.0", @@ -45,6 +46,7 @@ "@polkadot/util-crypto": "^13.0.0", "cbor-web": "^9.0.0", "tweetnacl": "^1.0.3", - "uuid": "^10.0.0" + "uuid": "^10.0.0", + "varint": "^6.0.0" } } diff --git a/packages/utils/src/Multikey.ts b/packages/utils/src/Multikey.ts new file mode 100644 index 000000000..4e14b21e8 --- /dev/null +++ b/packages/utils/src/Multikey.ts @@ -0,0 +1,184 @@ +/** + * Copyright (c) 2018-2024, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import { decodeBase58BtcMultikey } from '@kiltprotocol/jcs-data-integrity-proofs-common' +// @ts-expect-error Not a typescript module +import * as varint from 'varint' + +import { + Base58BtcMultibaseString, + KeyringPair, + MultibaseKeyPair, + MultibasePublicKey, + MultibaseSecretKey, + TypedKeypair, +} from '@kiltprotocol/types' +import { u8aConcat } from '@polkadot/util' +import { base58Encode } from '@polkadot/util-crypto' +import { DidError } from './SDKErrors.js' + +const MULTICODEC_SECP256K1_PREFIXES = [0xe7, 0x1301] as const +const MULTICODEC_X25519_PREFIXES = [0xec, 0x1302] as const +const MULTICODEC_ED25519_PREFIXES = [0xed, 0x1300] as const +const MULTICODEC_SR25519_PREFIXES = [0xef, 0x1303] as const + +type KeyTypeString = 'ed25519' | 'sr25519' | 'x25519' | 'secp256k1' + +export type KnownTypeString = KeyringPair['type'] | KeyTypeString + +function mapTypeStringToPrefixes(type: KnownTypeString): [number, number] { + switch (type) { + case 'ecdsa': + case 'secp256k1': + case 'ethereum': + return [...MULTICODEC_SECP256K1_PREFIXES] + case 'sr25519': + return [...MULTICODEC_SR25519_PREFIXES] + case 'x25519': + return [...MULTICODEC_X25519_PREFIXES] + case 'ed25519': + return [...MULTICODEC_ED25519_PREFIXES] + default: + throw new DidError(`The provided key type "${type}" is not supported.`) + } +} + +// TODO: This could also be exposed in a new release candidate of the `@kiltprotocol/jcs-data-integrity-proofs-common` package. +function multibase58BtcKeyBytesEncoding( + key: Uint8Array, + keyPrefix: number +): Base58BtcMultibaseString { + const varintEncodedPrefix = varint.encode(keyPrefix) + const prefixedKey = u8aConcat(varintEncodedPrefix, key) + const base58BtcEncodedKey = base58Encode(prefixedKey) + return `z${base58BtcEncodedKey}` +} + +type TypedKeypairWithOptionalSecretKey = Partial< + Pick, 'secretKey'> +> & + Pick, 'publicKey' | 'type'> + +/** + * Create a Multikey representation of a keypair, encoded in multibase-base58-btc, given its type and public/secret keys. + * + * @param keypair The input keypair to encode as Multikey. + * @param keypair.type The keypair type indicated by a type string. + * @param keypair.publicKey The keypair's public key bytes. + * @param keypair.secretKey The keypair's secret key bytes. + * @returns The Multikey representation (i.e., multicodec-prefixed, then base58-btc multibase encoded) of the provided keypair. + */ +export function encodeMultibaseKeypair( + args: TypedKeypair +): MultibaseKeyPair +/** + * Create a Multikey representation of a keypair's public key, encoded in multibase-base58-btc, given its type and public key bytes. + * + * @param keypair The input keypair to encode as Multikey. + * @param keypair.type The keypair type indicated by a type string. + * @param keypair.publicKey The keypair's public key bytes. + * @returns The Multikey representation (i.e., multicodec-prefixed, then base58-btc multibase encoded) of the public key. + */ +export function encodeMultibaseKeypair( + args: Pick, 'publicKey' | 'type'> +): MultibasePublicKey +// eslint-disable-next-line jsdoc/require-jsdoc -- Docs are provided to overloads. +export function encodeMultibaseKeypair({ + type, + publicKey, + secretKey, +}: TypedKeypairWithOptionalSecretKey): MultibasePublicKey & + Partial { + const [multiCodecPublicKeyPrefix, multiCodedSecretKeyPrefix] = + mapTypeStringToPrefixes(type) + + const keypair: MultibasePublicKey & Partial = { + publicKeyMultibase: multibase58BtcKeyBytesEncoding( + publicKey, + multiCodecPublicKeyPrefix + ), + } + if (secretKey) { + keypair.secretKeyMultibase = multibase58BtcKeyBytesEncoding( + secretKey, + multiCodedSecretKeyPrefix + ) + } + + return keypair +} + +const publicKeyPrefixes: Record = { + [MULTICODEC_SECP256K1_PREFIXES[0]]: 'secp256k1', + [MULTICODEC_X25519_PREFIXES[0]]: 'x25519', + [MULTICODEC_ED25519_PREFIXES[0]]: 'ed25519', + [MULTICODEC_SR25519_PREFIXES[0]]: 'sr25519', +} + +const secretKeyPrefixes: Record = { + [MULTICODEC_SECP256K1_PREFIXES[1]]: 'secp256k1', + [MULTICODEC_X25519_PREFIXES[1]]: 'x25519', + [MULTICODEC_ED25519_PREFIXES[1]]: 'ed25519', + [MULTICODEC_SR25519_PREFIXES[1]]: 'sr25519', +} + +/** + * Decodes a Multikey representation of a key pair, returning the public- and secret key bytes and the key type. + * Note that only base58-btc multibase encoding is currently supported. + * + * @param keyPairMultibase A key pair represented in Multikey format (i.e., multicodec-prefixed, then multibase encoded). + * @param keyPairMultibase.publicKeyMultibase The keypair's public key, encoded in Multikey format. + * @param keyPairMultibase.secretKeyMultibase The keypair's secret key, encoded in Multikey format. + * @returns The decoded `publicKey` and `secretKey` bytes plus a key `type`. + */ +export function decodeMultibaseKeypair( + keypair: MultibaseKeyPair +): TypedKeypair +/** + * Decodes a Multikey representation of a public key/verification method, returning the publics key bytes and the key type. + * Note that only base58-btc multibase encoding is currently supported. + * + * @param keyPairMultibase A verification method or comparable public key representation. + * @param keyPairMultibase.publicKeyMultibase The keypair's public key, multicodec-prefixed, then multibase encoded. + * @returns The decoded `publicKey` bytes plus a key `type`. + */ +export function decodeMultibaseKeypair( + keyPairPublicKey: MultibasePublicKey +): Pick, 'publicKey' | 'type'> +// eslint-disable-next-line jsdoc/require-jsdoc -- Docs are provided to overloads. +export function decodeMultibaseKeypair({ + publicKeyMultibase, + secretKeyMultibase, +}: MultibasePublicKey & + Partial): TypedKeypairWithOptionalSecretKey { + const { keyBytes, prefix } = decodeBase58BtcMultikey(publicKeyMultibase) + + const keyType = publicKeyPrefixes[prefix] + if (keyType === undefined) { + throw new DidError( + `Cannot decode key type for multibase key "${publicKeyMultibase}".` + ) + } + + const result: TypedKeypairWithOptionalSecretKey = { + type: keyType, + publicKey: keyBytes, + } + + if (typeof secretKeyMultibase === 'string') { + const decodedKey = decodeBase58BtcMultikey(secretKeyMultibase) + const secretKeyType = secretKeyPrefixes[decodedKey.prefix] + if (secretKeyType !== keyType) { + throw new Error( + `Secret key type ${secretKeyType} (prefix ${decodedKey.prefix}) does not match public key type ${keyType} (prefix ${prefix})` + ) + } + result.secretKey = decodedKey.keyBytes + } + + return result +} diff --git a/packages/utils/src/Signers.ts b/packages/utils/src/Signers.ts index c09e95a30..27c95f7b0 100644 --- a/packages/utils/src/Signers.ts +++ b/packages/utils/src/Signers.ts @@ -17,31 +17,39 @@ import { import { blake2AsU8a, encodeAddress, + randomAsHex, secp256k1Sign, } from '@polkadot/util-crypto' import type { Keypair } from '@polkadot/util-crypto/types' -import { - createSigner as es256kSigner, - cryptosuite as es256kSuite, -} from '@kiltprotocol/es256k-jcs-2023' import { createSigner as ed25519Signer, cryptosuite as ed25519Suite, } from '@kiltprotocol/eddsa-jcs-2022' import { - cryptosuite as sr25519Suite, + createSigner as es256kSigner, + cryptosuite as es256kSuite, +} from '@kiltprotocol/es256k-jcs-2023' +import { createSigner as sr25519Signer, + cryptosuite as sr25519Suite, } from '@kiltprotocol/sr25519-jcs-2023' import type { - SignerInterface, DidDocument, DidUrl, KeyringPair, + KiltKeyringPair, + MultibaseKeyPair, + SignerInterface, UriFragment, } from '@kiltprotocol/types' - +import { makeKeypairFromUri } from './Crypto.js' +import { + type KnownTypeString, + decodeMultibaseKeypair, + encodeMultibaseKeypair, +} from './Multikey.js' import { DidError, NoSuitableSignerError } from './SDKErrors.js' export const ALGORITHMS = Object.freeze({ @@ -126,10 +134,10 @@ export async function ethereumEcdsaSigner({ * @param pair The pair, where the private key is inaccessible. * @returns The private key as a byte sequence. */ -function extractPk(pair: KeyringPair): Uint8Array { +function extractPk(pair: Pick): Keypair { const encoded = pair.encodePkcs8() - const { secretKey } = decodePair(undefined, encoded, 'none') - return secretKey + const { secretKey, publicKey } = decodePair(undefined, encoded, 'none') + return { secretKey, publicKey } } const signerFactory = { @@ -170,30 +178,25 @@ export async function signerFromKeypair< throw new Error(`unknown algorithm ${algorithm}`) } - if (!('secretKey' in keypair) && 'encodePkcs8' in keypair) { - const signerId = id ?? (keypair.address as Id) - return { - id: signerId, - algorithm, - sign: async (signData) => { - // TODO: can probably be optimized; but care must be taken to respect keyring locking - const secretKey = extractPk(keypair) - const { sign } = await makeSigner({ - secretKey, - publicKey: keypair.publicKey, - id: signerId, - }) - return sign(signData) - }, - } + if ('secretKey' in keypair && 'publicKey' in keypair) { + const { secretKey, publicKey } = keypair + return makeSigner({ + secretKey, + publicKey, + id: id ?? (encodeAddress(publicKey, 38) as Id), + }) } - const { secretKey, publicKey } = keypair - return makeSigner({ - secretKey, - publicKey, - id: id ?? (encodeAddress(publicKey, 38) as Id), - }) + if ('encodePkcs8' in keypair) { + const { secretKey, publicKey } = extractPk(keypair) + return makeSigner({ + secretKey, + publicKey, + id: id ?? (keypair.address as Id), + }) + } + + throw new Error('') } function algsForKeyType(keyType: string): KnownAlgorithms[] { @@ -227,19 +230,26 @@ function algsForKeyType(keyType: string): KnownAlgorithms[] { export async function getSignersForKeypair({ id, keypair, - type = (keypair as KeyringPair).type, + type, }: { id?: Id - keypair: Keypair | KeyringPair + keypair: Keypair | KeyringPair | MultibaseKeyPair type?: string }): Promise>> { - if (!type) { + let pair: KeyringPair | (Keypair & { type: string }) + if ('publicKeyMultibase' in keypair) { + pair = decodeMultibaseKeypair(keypair) + } else if ('type' in keypair) { + pair = keypair + } else if (type) { + pair = { ...keypair, type } + } else { throw new Error('type is required if keypair.type is not given') } - const algorithms = algsForKeyType(type) + const algorithms = algsForKeyType(pair.type) return Promise.all( algorithms.map(async (algorithm) => { - return signerFromKeypair({ keypair, id, algorithm }) + return signerFromKeypair({ keypair: pair, id, algorithm }) }) ) } @@ -443,7 +453,10 @@ export function getPolkadotSigner( } const signature = await signer.sign({ data: signData }) // The signature is expected to be a SCALE enum, we must add a type prefix representing the signature algorithm - const prefixed = u8aConcat(TYPE_PREFIX[signer.algorithm], signature) + const prefixed = u8aConcat( + TYPE_PREFIX[signer.algorithm as keyof typeof TYPE_PREFIX], + signature + ) id += 1 return { id, @@ -452,3 +465,37 @@ export function getPolkadotSigner( }, } } + +/** + * Generates a Multikey encoded keypair from a seed or mnemonic. + * + * @param args Optional generator arguments. + * @param args.seed A 32 byte hex-encoded and 0x-prefixed seed or 12-word mnemonic, optionally postfixed with a derivation path (e.g., `//authentication-key`). + * This is case-insensitive. + * @param args.type A string indicating desired key type. + * @returns A pair of `publicKeyMultibase` & `secretKeyMultibase`. + */ +export function generateKeypair({ + seed = randomAsHex(32), + type = 'ed25519', +}: { + seed?: string + type?: KnownTypeString +} = {}): MultibaseKeyPair { + let typeForKeyring = type as KiltKeyringPair['type'] + switch (type.toLowerCase()) { + case 'secp256k1': + case 'ethereum': + typeForKeyring = 'ecdsa' + break + case 'x25519': + typeForKeyring = 'ed25519' + break + default: + } + + const keyRingPair = makeKeypairFromUri(seed.toLowerCase(), typeForKeyring) + + const { secretKey, publicKey } = extractPk(keyRingPair) + return encodeMultibaseKeypair({ publicKey, secretKey, type }) +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 6a0424605..ef52f484e 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -17,6 +17,7 @@ export * as DataUtils from './DataUtils.js' export * as SDKErrors from './SDKErrors.js' export * as JsonSchema from './json-schema/index.js' export * as Signers from './Signers.js' +export * as Multikey from './Multikey.js' export { Caip19, Caip2 } from './CAIP/index.js' export { ss58Format } from './ss58Format.js' export { cbor } from './cbor.js' diff --git a/tests/bundle/bundle-test.ts b/tests/bundle/bundle-test.ts index 7ecca615c..ca433f0c6 100644 --- a/tests/bundle/bundle-test.ts +++ b/tests/bundle/bundle-test.ts @@ -26,7 +26,7 @@ const { Holder, DidResolver, signAndSubmitTx, - signerFromKeypair, + getSignersForKeypair, } = kilt async function authorizeTx( @@ -63,9 +63,9 @@ async function createFullDid( ) { const api = ConfigService.get('api') - const signer: SignerInterface = await signerFromKeypair({ + const [signer] = await getSignersForKeypair({ keypair, - algorithm: 'Ed25519', + type: 'Ed25519', }) const address = signer.id const getSigners: ( @@ -129,10 +129,10 @@ async function runAll() { 3, ]), } - const payerSigner = await signerFromKeypair<'Ed25519', KiltAddress>({ + const [payerSigner] = (await getSignersForKeypair({ keypair: faucet, - algorithm: 'Ed25519', - }) + type: 'Ed25519', + })) as Array> console.log('faucet signer created') @@ -281,9 +281,9 @@ async function runAll() { } const issued = await Issuer.issue(credential, { - did: alice.id, - signers: [...(await aliceSign(alice)), payerSigner], - submitterAccount: payerSigner.id, + didDocument: alice, + signers: [...aliceSign(alice), payerSigner], + submitter: payerSigner.id, }) console.info('Credential issued') @@ -312,8 +312,8 @@ async function runAll() { const presentation = await Holder.createPresentation( [derived], { - did: bob.id, - signers: await bobSign(bob), + didDocument: bob, + signers: bobSign(bob), }, {}, { diff --git a/tests/integration/Did.spec.ts b/tests/integration/Did.spec.ts index a6f6cdab4..0637d2bd8 100644 --- a/tests/integration/Did.spec.ts +++ b/tests/integration/Did.spec.ts @@ -21,7 +21,7 @@ import { CType, DelegationNode } from '@kiltprotocol/credentials' import * as Did from '@kiltprotocol/did' import { disconnect } from '@kiltprotocol/sdk-js' import { Permission } from '@kiltprotocol/types' -import { UUID } from '@kiltprotocol/utils' +import { Multikey, UUID } from '@kiltprotocol/utils' import type { KeyTool } from '../testUtils/index.js' @@ -137,10 +137,10 @@ describe('write and didDeleteTx', () => { controller: fullDid, type: 'Multikey', // We cannot match the ID of the key because it will be defined by the blockchain while saving - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ type: 'sr25519', publicKey: authPublicKey, - }), + }).publicKeyMultibase, }), ], }) @@ -370,13 +370,15 @@ describe('DID migration', () => { controller: migratedFullDid, type: 'Multikey', // We cannot match the ID of the key because it will be defined by the blockchain while saving - publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), + publicKeyMultibase: Multikey.encodeMultibaseKeypair(authentication[0]) + .publicKeyMultibase, }), expect.objectContaining(>{ controller: migratedFullDid, type: 'Multikey', // We cannot match the ID of the key because it will be defined by the blockchain while saving - publicKeyMultibase: Did.keypairToMultibaseKey(keyAgreement[0]), + publicKeyMultibase: Multikey.encodeMultibaseKeypair(keyAgreement[0]) + .publicKeyMultibase, }), ]), }) @@ -427,7 +429,8 @@ describe('DID migration', () => { controller: migratedFullDid, type: 'Multikey', // We cannot match the ID of the key because it will be defined by the blockchain while saving - publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), + publicKeyMultibase: Multikey.encodeMultibaseKeypair(authentication[0]) + .publicKeyMultibase, }), ], }) @@ -492,13 +495,15 @@ describe('DID migration', () => { controller: migratedFullDid, type: 'Multikey', // We cannot match the ID of the key because it will be defined by the blockchain while saving - publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), + publicKeyMultibase: Multikey.encodeMultibaseKeypair(authentication[0]) + .publicKeyMultibase, }), expect.objectContaining(>{ controller: migratedFullDid, type: 'Multikey', // We cannot match the ID of the key because it will be defined by the blockchain while saving - publicKeyMultibase: Did.keypairToMultibaseKey(keyAgreement[0]), + publicKeyMultibase: Multikey.encodeMultibaseKeypair(keyAgreement[0]) + .publicKeyMultibase, }), ]), service: [ @@ -563,9 +568,9 @@ describe('DID authorization', () => { await submitTx(createTx, paymentAccount) const didLinkedInfo = await api.call.did.query( Did.toChain( - Did.getFullDidFromVerificationMethod({ - publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), - }) + Did.getFullDidFromVerificationMethod( + Multikey.encodeMultibaseKeypair(authentication[0]) + ) ) ) did = Did.linkedInfoFromChain(didLinkedInfo).document @@ -674,9 +679,9 @@ describe('DID management batching', () => { await submitTx(extrinsic, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( Did.toChain( - Did.getFullDidFromVerificationMethod({ - publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), - }) + Did.getFullDidFromVerificationMethod( + Multikey.encodeMultibaseKeypair(authentication[0]) + ) ) ) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) @@ -688,52 +693,54 @@ describe('DID management batching', () => { // Authentication controller: fullDid.id, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), + publicKeyMultibase: Multikey.encodeMultibaseKeypair( + authentication[0] + ).publicKeyMultibase, }), // Assertion method expect.objectContaining({ controller: fullDid.id, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ type: 'sr25519', publicKey: new Uint8Array(32).fill(1), - }), + }).publicKeyMultibase, }), // Capability delegation expect.objectContaining({ controller: fullDid.id, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ type: 'ecdsa', publicKey: new Uint8Array(33).fill(1), - }), + }).publicKeyMultibase, }), // Key agreement 1 expect.objectContaining({ controller: fullDid.id, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ type: 'x25519', publicKey: new Uint8Array(32).fill(1), - }), + }).publicKeyMultibase, }), // Key agreement 2 expect.objectContaining({ controller: fullDid.id, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ type: 'x25519', publicKey: new Uint8Array(32).fill(2), - }), + }).publicKeyMultibase, }), // Key agreement 3 expect.objectContaining({ controller: fullDid.id, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ type: 'x25519', publicKey: new Uint8Array(32).fill(3), - }), + }).publicKeyMultibase, }), ]) ) @@ -778,9 +785,9 @@ describe('DID management batching', () => { const fullDidLinkedInfo = await api.call.did.query( Did.toChain( - Did.getFullDidFromVerificationMethod({ - publicKeyMultibase: Did.keypairToMultibaseKey(didAuthKey), - }) + Did.getFullDidFromVerificationMethod( + Multikey.encodeMultibaseKeypair(didAuthKey) + ) ) ) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) @@ -792,7 +799,8 @@ describe('DID management batching', () => { expect.objectContaining(>{ controller: fullDid.id, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey(didAuthKey), + publicKeyMultibase: + Multikey.encodeMultibaseKeypair(didAuthKey).publicKeyMultibase, }), ], }) @@ -853,9 +861,9 @@ describe('DID management batching', () => { const initialFullDidLinkedInfo = await api.call.did.query( Did.toChain( - Did.getFullDidFromVerificationMethod({ - publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), - }) + Did.getFullDidFromVerificationMethod( + Multikey.encodeMultibaseKeypair(authentication[0]) + ) ) ) const { document: initialFullDid } = Did.linkedInfoFromChain( @@ -908,10 +916,10 @@ describe('DID management batching', () => { expect.objectContaining(>{ controller: finalFullDid.id, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ publicKey: keypair.publicKey, type: 'sr25519', - }), + }).publicKeyMultibase, }), ], }) @@ -937,9 +945,9 @@ describe('DID management batching', () => { const initialFullDidLinkedInfo = await api.call.did.query( Did.toChain( - Did.getFullDidFromVerificationMethod({ - publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), - }) + Did.getFullDidFromVerificationMethod( + Multikey.encodeMultibaseKeypair(authentication[0]) + ) ) ) const { document: initialFullDid } = Did.linkedInfoFromChain( @@ -986,10 +994,10 @@ describe('DID management batching', () => { expect.objectContaining(>{ controller: finalFullDid.id, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKeyMultibase: Multikey.encodeMultibaseKeypair({ publicKey: newAuthKey.publicKey, type: 'ed25519', - }), + }).publicKeyMultibase, }), ], service: [ @@ -1033,9 +1041,9 @@ describe('DID management batching', () => { await submitTx(tx, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( Did.toChain( - Did.getFullDidFromVerificationMethod({ - publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), - }) + Did.getFullDidFromVerificationMethod( + Multikey.encodeMultibaseKeypair(authentication[0]) + ) ) ) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) @@ -1075,7 +1083,9 @@ describe('DID management batching', () => { // Authentication and assertionMethod controller: fullDid.id, type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), + publicKeyMultibase: Multikey.encodeMultibaseKeypair( + authentication[0] + ).publicKeyMultibase, }), ], // Old service maintained @@ -1117,9 +1127,9 @@ describe('DID management batching', () => { await submitTx(createTx, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( Did.toChain( - Did.getFullDidFromVerificationMethod({ - publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), - }) + Did.getFullDidFromVerificationMethod( + Multikey.encodeMultibaseKeypair(authentication[0]) + ) ) ) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) diff --git a/tests/integration/didHelpers.spec.ts b/tests/integration/didHelpers.spec.ts new file mode 100644 index 000000000..3e7c04b01 --- /dev/null +++ b/tests/integration/didHelpers.spec.ts @@ -0,0 +1,435 @@ +/** + * Copyright (c) 2018-2024, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import type { ApiPromise } from '@polkadot/api' + +import { CType } from '@kiltprotocol/credentials' +import { getFullDidFromVerificationMethod } from '@kiltprotocol/did' +import { + DidHelpers, + disconnect, + generateKeypair, + Issuer, + Verifier, +} from '@kiltprotocol/sdk-js' +import type { + DidDocument, + KeyringPair, + KiltKeyringPair, + MultibaseKeyPair, + Service, +} from '@kiltprotocol/types' +import { Crypto } from '@kiltprotocol/utils' + +import { createEndowedTestAccount, initializeApi } from './utils.js' + +let api: ApiPromise +beforeAll(async () => { + api = await initializeApi() +}, 30_000) + +let paymentAccount: KiltKeyringPair +beforeAll(async () => { + paymentAccount = await createEndowedTestAccount() +}, 30_000) + +// Create did on chain +describe('create and deactivate DID', () => { + let kp: MultibaseKeyPair + let didDocument: DidDocument + beforeAll(() => { + kp = generateKeypair({ + seed: 'build hill second flame trigger simple rigid cabbage phrase evolve final eight', + type: 'sr25519', + }) + }) + + it('creates a DID', async () => { + const result = await DidHelpers.createDid({ + api, + signers: [kp], + submitter: paymentAccount, + fromPublicKey: kp, + }).submit() + + expect(result.status).toBe('confirmed') + didDocument = result.asConfirmed.didDocument + const did = getFullDidFromVerificationMethod(kp) + expect(didDocument).toMatchObject({ + id: did, + verificationMethod: expect.any(Array), + authentication: expect.any(Array), + }) + expect(didDocument.verificationMethod).toHaveProperty('length', 1) + expect(didDocument.authentication).toHaveProperty('length', 1) + }, 30_000) + + it('deactivates the DID', async () => { + const result = await DidHelpers.deactivateDid({ + api, + signers: [kp], + submitter: paymentAccount, + didDocument, + }).submit() + + expect(result.status).toBe('confirmed') + const updatedDoc = result.asConfirmed.didDocument + expect(updatedDoc.id).toStrictEqual(didDocument.id) + expect(updatedDoc).not.toMatchObject(didDocument) + expect(updatedDoc).not.toHaveProperty('authentication') + expect(updatedDoc).not.toHaveProperty('verificationMethod') + }, 30_000) +}) + +describe('w3ns', () => { + let keypair: MultibaseKeyPair + let didDocument: DidDocument + beforeAll(async () => { + keypair = generateKeypair({ seed: '//Blob' }) + const result = await DidHelpers.createDid({ + api, + signers: [keypair], + submitter: paymentAccount, + fromPublicKey: keypair, + }).submit() + didDocument = result.asConfirmed.didDocument + }) + + it('claims w3n', async () => { + const result = await DidHelpers.claimWeb3Name({ + api, + signers: [keypair], + submitter: paymentAccount, + didDocument, + name: 'blob', + }).submit() + expect(result.status).toStrictEqual('confirmed') + didDocument = result.asConfirmed.didDocument + expect(didDocument).toHaveProperty( + 'alsoKnownAs', + expect.arrayContaining(['w3n:blob']) + ) + }, 30_000) + + it('fails when trying to claim a 2nd w3n', async () => { + const result = await DidHelpers.claimWeb3Name({ + api, + signers: [keypair], + submitter: paymentAccount, + didDocument, + name: 'blarb', + }).submit() + + expect(result.status).toStrictEqual('failed') + expect(result.asFailed.error).toMatchInlineSnapshot( + `[Error: web3Names.OwnerAlreadyExists: The specified owner already owns a name.]` + ) + expect(result.asFailed.didDocument).toMatchObject(didDocument) + }, 30_000) + + it('releases a w3n', async () => { + const result = await DidHelpers.releaseWeb3Name({ + api, + signers: [keypair], + submitter: paymentAccount, + didDocument, + }).submit() + + expect(result.status).toStrictEqual('confirmed') + didDocument = result.asConfirmed.didDocument + expect(didDocument).not.toHaveProperty('alsoKnownAs') + }, 30_000) +}) + +describe('services', () => { + let keypair: MultibaseKeyPair + let didDocument: DidDocument + beforeAll(async () => { + keypair = generateKeypair({ seed: '//Services' }) + const result = await DidHelpers.createDid({ + api, + signers: [keypair], + submitter: paymentAccount, + fromPublicKey: keypair, + }).submit() + didDocument = result.asConfirmed.didDocument + }) + + it('adds a service', async () => { + const result = await DidHelpers.addService({ + api, + signers: [keypair], + submitter: paymentAccount, + didDocument, + service: { + id: '#my_service', + type: ['http://schema.org/EmailService'], + serviceEndpoint: ['mailto:info@kilt.io'], + }, + }).submit() + expect(result.status).toStrictEqual('confirmed') + didDocument = result.asConfirmed.didDocument + expect(didDocument).toHaveProperty( + 'service', + expect.arrayContaining([ + { + id: `${didDocument.id}#my_service`, + type: ['http://schema.org/EmailService'], + serviceEndpoint: ['mailto:info@kilt.io'], + }, + ]) + ) + expect(didDocument.service).toHaveLength(1) + }, 30_000) + + it('removes a service', async () => { + const result = await DidHelpers.removeService({ + api, + signers: [keypair], + submitter: paymentAccount, + didDocument, + id: '#my_service', + }).submit() + + expect(result.status).toStrictEqual('confirmed') + didDocument = result.asConfirmed.didDocument + expect(didDocument).not.toHaveProperty('service') + }, 30_000) +}) + +describe('verification methods', () => { + let keypair: KeyringPair + let didDocument: DidDocument + beforeAll(async () => { + keypair = Crypto.makeKeypairFromUri('//Vms') + const result = await DidHelpers.createDid({ + api, + signers: [keypair], + submitter: paymentAccount, + fromPublicKey: keypair, + }).submit() + didDocument = result.asConfirmed.didDocument + }) + + it('sets an assertion method', async () => { + expect(didDocument).not.toHaveProperty('assertionMethod') + const result = await DidHelpers.setVerificationMethod({ + api, + signers: [keypair], + submitter: paymentAccount, + didDocument, + publicKey: keypair, + relationship: 'assertionMethod', + }).submit() + expect(result.status).toStrictEqual('confirmed') + didDocument = result.asConfirmed.didDocument + expect(didDocument).toHaveProperty( + 'assertionMethod', + didDocument.authentication + ) + + const result2 = await DidHelpers.setVerificationMethod({ + api, + signers: [keypair], + submitter: paymentAccount, + didDocument, + publicKey: { publicKey: new Uint8Array(32).fill(1), type: 'ed25519' }, + relationship: 'assertionMethod', + }).submit() + + expect(result2.status).toStrictEqual('confirmed') + didDocument = result2.asConfirmed.didDocument + expect(didDocument.assertionMethod).toHaveLength(1) + expect(didDocument.assertionMethod![0]).not.toEqual( + didDocument.authentication![0] + ) + }, 60_000) + + it('sets a key agreement method', async () => { + expect(didDocument).not.toHaveProperty('keyAgreement') + const result = await DidHelpers.setVerificationMethod({ + api, + signers: [keypair], + submitter: paymentAccount, + didDocument, + publicKey: { publicKey: new Uint8Array(32).fill(0), type: 'x25519' }, + relationship: 'keyAgreement', + }).submit() + expect(result.status).toStrictEqual('confirmed') + didDocument = result.asConfirmed.didDocument + expect(didDocument).toHaveProperty('keyAgreement', expect.any(Array)) + expect(didDocument.keyAgreement).toHaveLength(1) + + const [oldKey] = didDocument.keyAgreement! + + const result2 = await DidHelpers.setVerificationMethod({ + api, + signers: [keypair], + submitter: paymentAccount, + didDocument, + publicKey: { publicKey: new Uint8Array(32).fill(1), type: 'x25519' }, + relationship: 'keyAgreement', + }).submit() + + expect(result2.status).toStrictEqual('confirmed') + didDocument = result2.asConfirmed.didDocument + expect(didDocument.keyAgreement).toHaveLength(1) + expect(didDocument.keyAgreement![0]).not.toEqual(oldKey) + }, 60_000) + + it('removes an assertion method', async () => { + expect(didDocument.assertionMethod).toHaveLength(1) + + const result = await DidHelpers.removeVerificationMethod({ + api, + signers: [keypair], + submitter: paymentAccount, + didDocument, + verificationMethodId: didDocument.assertionMethod![0], + relationship: 'assertionMethod', + }).submit() + + expect(result.status).toStrictEqual('confirmed') + didDocument = result.asConfirmed.didDocument + expect(didDocument).not.toHaveProperty('assertionMethod') + }, 30_000) + + it('removes a key agreement method', async () => { + expect(didDocument.keyAgreement).toHaveLength(1) + + const result = await DidHelpers.removeVerificationMethod({ + api, + signers: [keypair], + submitter: paymentAccount, + didDocument, + verificationMethodId: didDocument.keyAgreement![0], + relationship: 'keyAgreement', + }).submit() + + expect(result.status).toStrictEqual('confirmed') + didDocument = result.asConfirmed.didDocument + expect(didDocument).not.toHaveProperty('keyAgreement') + }, 30_000) + + it('fails to remove authentication method', async () => { + await expect( + Promise.resolve() + .then(() => + DidHelpers.removeVerificationMethod({ + api, + signers: [keypair], + submitter: paymentAccount, + didDocument, + verificationMethodId: didDocument.authentication![0], + relationship: 'authentication', + }).submit() + ) + .then(({ asConfirmed }) => asConfirmed) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"authentication verification methods can not be removed"` + ) + }, 30_000) +}) + +describe('transact', () => { + let keypair: MultibaseKeyPair + let didDocument: DidDocument + beforeAll(async () => { + keypair = generateKeypair({ seed: '//Transact' }) + didDocument = ( + await DidHelpers.createDid({ + api, + signers: [keypair], + submitter: paymentAccount, + fromPublicKey: keypair, + }).submit() + ).asConfirmed.didDocument + + didDocument = ( + await DidHelpers.setVerificationMethod({ + api, + signers: [keypair], + submitter: paymentAccount, + didDocument, + publicKey: keypair, + relationship: 'assertionMethod', + }).submit() + ).asConfirmed.didDocument + }) + + const cType = CType.fromProperties('thing', { + someProperty: { type: 'string' }, + }) + + it('creates a ctype', async () => { + const serialized = CType.toChain(cType) + const call = api.tx.ctype.add(serialized) + + const result = await DidHelpers.transact({ + api, + signers: [keypair], + submitter: paymentAccount, + didDocument, + call, + expectedEvents: [{ section: 'ctype', method: 'CTypeCreated' }], + }).submit() + + expect(result.status).toStrictEqual('confirmed') + expect(result.asConfirmed.didDocument).toMatchObject(didDocument) + await expect(CType.verifyStored(cType)).resolves.not.toThrow() + }, 30_000) + + it('integrates with issue', async () => { + const unsigned = await Issuer.createCredential({ + issuer: didDocument.id, + credentialSubject: { id: didDocument.id, someProperty: 'someValue' }, + cType, + }) + + const issued = await Issuer.issue(unsigned, { + didDocument, + signers: [keypair], + submitter: async (args) => + DidHelpers.transact({ + ...args, + submitter: paymentAccount, + expectedEvents: [ + { section: 'attestation', method: 'AttestationCreated' }, + ], + }).submit(), + }) + + expect(issued).toHaveProperty('proof', expect.any(Object)) + + await expect( + Verifier.verifyCredential({ credential: issued }) + ).resolves.toMatchObject({ verified: true }) + }, 30_000) + + it('creates signed submittable', async () => { + const serialized = CType.toChain(cType) + const call = api.tx.ctype.add(serialized) + + const { txHex } = await DidHelpers.transact({ + api, + signers: [keypair], + submitter: paymentAccount, + didDocument, + call, + expectedEvents: [{ section: 'ctype', method: 'CTypeCreated' }], + }).getSubmittable({ signSubmittable: true }) + expect(txHex).toContain('0x') + const parsed = api.tx(txHex) + expect(parsed.method).toHaveProperty('section', 'did') + expect(parsed.method).toHaveProperty('method', 'submitDidCall') + expect(parsed.isSigned).toBe(true) + }, 30_000) +}) + +afterAll(async () => { + await disconnect() +}) diff --git a/tests/integration/utils.ts b/tests/integration/utils.ts index 6a20a9ffc..be42dd2cc 100644 --- a/tests/integration/utils.ts +++ b/tests/integration/utils.ts @@ -24,6 +24,7 @@ import type { KiltKeyringPair, SubmittableExtrinsic, SubscriptionPromise, + TransactionSigner, } from '@kiltprotocol/types' import { Crypto } from '@kiltprotocol/utils' @@ -110,7 +111,7 @@ export const devBob = Crypto.makeKeypairFromUri('//Bob') export const devCharlie = Crypto.makeKeypairFromUri('//Charlie') export function addressFromRandom(): KiltAddress { - return encodeAddress(randomAsU8a()) + return encodeAddress(randomAsU8a(), 38) } export async function isCtypeOnChain(cType: ICType): Promise { @@ -155,7 +156,7 @@ export const nftNameCType = CType.fromProperties('NFT collection name', { // Submits resolving when IS_IN_BLOCK export async function submitTx( extrinsic: SubmittableExtrinsic, - submitter: KeyringPair | Blockchain.TransactionSigner, + submitter: KeyringPair | TransactionSigner, resolveOn?: SubscriptionPromise.ResultEvaluator ): Promise { await Blockchain.signAndSubmitTx(extrinsic, submitter, { @@ -164,7 +165,7 @@ export async function submitTx( } export async function endowAccounts( - faucet: KeyringPair | Blockchain.TransactionSigner, + faucet: KeyringPair | TransactionSigner, addresses: string[], resolveOn?: SubscriptionPromise.ResultEvaluator ): Promise { diff --git a/tests/testUtils/TestUtils.ts b/tests/testUtils/TestUtils.ts index a56399cea..ad5212a77 100644 --- a/tests/testUtils/TestUtils.ts +++ b/tests/testUtils/TestUtils.ts @@ -16,13 +16,14 @@ import type { KiltKeyringPair, SignerInterface, SubmittableExtrinsic, + TransactionSigner, UriFragment, VerificationMethod, VerificationRelationship, } from '@kiltprotocol/types' -import { ConfigService } from '@kiltprotocol/sdk-js' +import { ConfigService } from '@kiltprotocol/config' import { Blockchain } from '@kiltprotocol/chain-helpers' -import { Crypto, Signers, SDKErrors } from '@kiltprotocol/utils' +import { Crypto, Signers, SDKErrors, Multikey } from '@kiltprotocol/utils' import { type BaseNewDidKey, type ChainDidKey, @@ -35,7 +36,6 @@ import { getStoreTx, didKeyToVerificationMethod, createLightDidDocument, - keypairToMultibaseKey, getFullDidFromVerificationMethod, multibaseKeyToDidKey, isValidDidVerificationType, @@ -248,12 +248,12 @@ export async function createLocalDemoFullDidFromKeypair( publicKey, id: authKeyFragment, } = makeDidKeyFromKeypair(keypair) - const id = getFullDidFromVerificationMethod({ - publicKeyMultibase: keypairToMultibaseKey({ + const id = getFullDidFromVerificationMethod( + Multikey.encodeMultibaseKeypair({ type: keyType, publicKey, - }), - }) + }) + ) const authKeyId = `${id}${authKeyFragment}` as const const result: DidDocument = { @@ -422,7 +422,7 @@ export async function getStoreTxFromDidDocument( // It takes the auth key from the light DID and use it as attestation and delegation key as well. export async function createFullDidFromLightDid( - payer: KiltKeyringPair | Blockchain.TransactionSigner, + payer: KiltKeyringPair | TransactionSigner, lightDidForId: DidDocument, signer: StoreDidCallback ): Promise { diff --git a/yarn.lock b/yarn.lock index 50b27b8c1..5a9ffb534 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2072,10 +2072,10 @@ __metadata: "@kiltprotocol/chain-helpers": "workspace:*" "@kiltprotocol/config": "workspace:*" "@kiltprotocol/did": "workspace:*" - "@kiltprotocol/eddsa-jcs-2022": "npm:0.1.0-rc.3" - "@kiltprotocol/es256k-jcs-2023": "npm:0.1.0-rc.3" - "@kiltprotocol/jcs-data-integrity-proofs-common": "npm:0.1.0-rc.3" - "@kiltprotocol/sr25519-jcs-2023": "npm:0.1.0-rc.3" + "@kiltprotocol/eddsa-jcs-2022": "npm:0.1.0-rc.4" + "@kiltprotocol/es256k-jcs-2023": "npm:0.1.0-rc.4" + "@kiltprotocol/jcs-data-integrity-proofs-common": "npm:0.1.0-rc.4" + "@kiltprotocol/sr25519-jcs-2023": "npm:0.1.0-rc.4" "@kiltprotocol/types": "workspace:*" "@kiltprotocol/utils": "workspace:*" "@polkadot/api": "npm:^12.0.0" @@ -2102,7 +2102,6 @@ __metadata: "@digitalbazaar/multikey-context": "npm:^2.0.1" "@digitalbazaar/security-context": "npm:^1.0.1" "@kiltprotocol/config": "workspace:*" - "@kiltprotocol/jcs-data-integrity-proofs-common": "npm:0.1.0-rc.3" "@kiltprotocol/types": "workspace:*" "@kiltprotocol/utils": "workspace:*" "@polkadot/api": "npm:^12.0.0" @@ -2112,7 +2111,6 @@ __metadata: "@polkadot/util-crypto": "npm:^13.0.0" rimraf: "npm:^3.0.2" typescript: "npm:^4.8.3" - varint: "npm:^6.0.0" peerDependencies: "@kiltprotocol/augment-api": "*" peerDependenciesMeta: @@ -2121,38 +2119,38 @@ __metadata: languageName: unknown linkType: soft -"@kiltprotocol/eddsa-jcs-2022@npm:0.1.0-rc.3": - version: 0.1.0-rc.3 - resolution: "@kiltprotocol/eddsa-jcs-2022@npm:0.1.0-rc.3" +"@kiltprotocol/eddsa-jcs-2022@npm:0.1.0-rc.4": + version: 0.1.0-rc.4 + resolution: "@kiltprotocol/eddsa-jcs-2022@npm:0.1.0-rc.4" dependencies: - "@kiltprotocol/jcs-data-integrity-proofs-common": "npm:^0.1.0-rc.3" + "@kiltprotocol/jcs-data-integrity-proofs-common": "npm:^0.1.0-rc.4" "@noble/curves": "npm:^1.0.0" "@scure/base": "npm:^1.1.1" - checksum: 10c0/b29228114faf0322bf21a372c2c4c99379d9d9006bb9a94a7029211bce47321f185ea2fe8182499ac4bcdaa5dd4aad0eafccea1e22cfbcb16058be17a0c3c164 + checksum: 10c0/a4a974e225cb90d17ca1902e6dc40bb0b71826d0d898ed4bad2c10be72938908fa480cfba040badf2754778a8e78843ce4262110c3360138d1f5aff1527f2a42 languageName: node linkType: hard -"@kiltprotocol/es256k-jcs-2023@npm:0.1.0-rc.3": - version: 0.1.0-rc.3 - resolution: "@kiltprotocol/es256k-jcs-2023@npm:0.1.0-rc.3" +"@kiltprotocol/es256k-jcs-2023@npm:0.1.0-rc.4": + version: 0.1.0-rc.4 + resolution: "@kiltprotocol/es256k-jcs-2023@npm:0.1.0-rc.4" dependencies: - "@kiltprotocol/jcs-data-integrity-proofs-common": "npm:^0.1.0-rc.3" + "@kiltprotocol/jcs-data-integrity-proofs-common": "npm:^0.1.0-rc.4" "@noble/curves": "npm:^1.0.0" "@scure/base": "npm:^1.1.1" - checksum: 10c0/02347bc7f199833b35995f89f4c13b02211ada54707f086896f841dd64bee1edf332c4e5139343d3d89b990c21fafd27de633e2c5a9c7106b6d5b91901e3d53e + checksum: 10c0/ff9f6a78494463d2be82e5a0df73be0df488f61bc560126b07332720772b6717944df744fb95a4bc99d08816ada23e21a540743de38f52c6c8b9b7213d7336f1 languageName: node linkType: hard -"@kiltprotocol/jcs-data-integrity-proofs-common@npm:0.1.0-rc.3, @kiltprotocol/jcs-data-integrity-proofs-common@npm:^0.1.0-rc.3": - version: 0.1.0-rc.3 - resolution: "@kiltprotocol/jcs-data-integrity-proofs-common@npm:0.1.0-rc.3" +"@kiltprotocol/jcs-data-integrity-proofs-common@npm:0.1.0-rc.4, @kiltprotocol/jcs-data-integrity-proofs-common@npm:^0.1.0-rc.4": + version: 0.1.0-rc.4 + resolution: "@kiltprotocol/jcs-data-integrity-proofs-common@npm:0.1.0-rc.4" dependencies: "@noble/hashes": "npm:^1.3.0" canonicalize: "npm:^2.0.0" varint: "npm:^6.0.0" peerDependencies: "@scure/base": ^1.1.0 - checksum: 10c0/0006f1d429bf34b4a1619f344cd194556906d9988cebf6f6d28a730689ca4e7c24e0dcff25f5f03150210f6fcb97b87d014af7cd80a55200baecbc3abcd1491e + checksum: 10c0/fb9e2ab04bdd2d362ac33b1362231cdd74fb03d0f1a9700adc5bf60e938127906aede2290ce86c4681c53dd28e7e4951bbec82f9fc120c264e47d2f99f46d2f6 languageName: node linkType: hard @@ -2205,7 +2203,9 @@ __metadata: "@kiltprotocol/did": "workspace:*" "@kiltprotocol/type-definitions": "npm:^0.35.0" "@kiltprotocol/utils": "workspace:*" + "@polkadot/api": "npm:^12.0.0" "@polkadot/typegen": "npm:^12.0.0" + "@polkadot/util": "npm:^13.0.0" rimraf: "npm:^3.0.2" terser-webpack-plugin: "npm:^5.1.1" typescript: "npm:^4.8.3" @@ -2216,14 +2216,14 @@ __metadata: languageName: unknown linkType: soft -"@kiltprotocol/sr25519-jcs-2023@npm:0.1.0-rc.3": - version: 0.1.0-rc.3 - resolution: "@kiltprotocol/sr25519-jcs-2023@npm:0.1.0-rc.3" +"@kiltprotocol/sr25519-jcs-2023@npm:0.1.0-rc.4": + version: 0.1.0-rc.4 + resolution: "@kiltprotocol/sr25519-jcs-2023@npm:0.1.0-rc.4" dependencies: - "@kiltprotocol/jcs-data-integrity-proofs-common": "npm:^0.1.0-rc.3" - "@polkadot/util-crypto": "npm:^12.0.1" + "@kiltprotocol/jcs-data-integrity-proofs-common": "npm:^0.1.0-rc.4" + "@polkadot/util-crypto": "npm:^13.0.2" "@scure/base": "npm:^1.1.1" - checksum: 10c0/789d06939bed32e41f4543427f3eb32773fd39385ff1f09ff9df345f620c1fe67a1e249e1afef29f4b054e2a79388ff1fea59dd26136565d95f7bf7bda248c0f + checksum: 10c0/07a6749813f9e45d14c19c2bfa1af7be8dc1bcff82c7b2d3b6c7e9d67ed3862be69372e51b6d1cbabcdb2ac2906b07c14cf8b492fad944aa1dec7cba2e3b0635 languageName: node linkType: hard @@ -2252,9 +2252,10 @@ __metadata: version: 0.0.0-use.local resolution: "@kiltprotocol/utils@workspace:packages/utils" dependencies: - "@kiltprotocol/eddsa-jcs-2022": "npm:0.1.0-rc.3" - "@kiltprotocol/es256k-jcs-2023": "npm:0.1.0-rc.3" - "@kiltprotocol/sr25519-jcs-2023": "npm:0.1.0-rc.3" + "@kiltprotocol/eddsa-jcs-2022": "npm:0.1.0-rc.4" + "@kiltprotocol/es256k-jcs-2023": "npm:0.1.0-rc.4" + "@kiltprotocol/jcs-data-integrity-proofs-common": "npm:0.1.0-rc.4" + "@kiltprotocol/sr25519-jcs-2023": "npm:0.1.0-rc.4" "@kiltprotocol/types": "workspace:*" "@polkadot/api": "npm:^12.0.0" "@polkadot/keyring": "npm:^13.0.0" @@ -2266,6 +2267,7 @@ __metadata: tweetnacl: "npm:^1.0.3" typescript: "npm:^4.8.3" uuid: "npm:^10.0.0" + varint: "npm:^6.0.0" languageName: unknown linkType: soft @@ -2500,17 +2502,6 @@ __metadata: languageName: node linkType: hard -"@polkadot/networks@npm:12.6.2": - version: 12.6.2 - resolution: "@polkadot/networks@npm:12.6.2" - dependencies: - "@polkadot/util": "npm:12.6.2" - "@substrate/ss58-registry": "npm:^1.44.0" - tslib: "npm:^2.6.2" - checksum: 10c0/44a482c46900058e6d5b25110cb5396382036057240cd4a8e0dae325fab54e689ec81bc43b047570581f14ce456b67310c05c1fe34c4b7f7d4e064f095f4c276 - languageName: node - linkType: hard - "@polkadot/networks@npm:13.0.2, @polkadot/networks@npm:^13.0.2": version: 13.0.2 resolution: "@polkadot/networks@npm:13.0.2" @@ -2696,41 +2687,6 @@ __metadata: languageName: node linkType: hard -"@polkadot/util-crypto@npm:^12.0.1": - version: 12.6.2 - resolution: "@polkadot/util-crypto@npm:12.6.2" - dependencies: - "@noble/curves": "npm:^1.3.0" - "@noble/hashes": "npm:^1.3.3" - "@polkadot/networks": "npm:12.6.2" - "@polkadot/util": "npm:12.6.2" - "@polkadot/wasm-crypto": "npm:^7.3.2" - "@polkadot/wasm-util": "npm:^7.3.2" - "@polkadot/x-bigint": "npm:12.6.2" - "@polkadot/x-randomvalues": "npm:12.6.2" - "@scure/base": "npm:^1.1.5" - tslib: "npm:^2.6.2" - peerDependencies: - "@polkadot/util": 12.6.2 - checksum: 10c0/b25f1574a2d4298c32b7a3cf3fa9f1b1237af3cc9e4ac16e75840097e9bcea11c8188abd5c46522d46d350edceb1e3e54fe8cbb01111e4eb643df4040ff41e2a - languageName: node - linkType: hard - -"@polkadot/util@npm:12.6.2": - version: 12.6.2 - resolution: "@polkadot/util@npm:12.6.2" - dependencies: - "@polkadot/x-bigint": "npm:12.6.2" - "@polkadot/x-global": "npm:12.6.2" - "@polkadot/x-textdecoder": "npm:12.6.2" - "@polkadot/x-textencoder": "npm:12.6.2" - "@types/bn.js": "npm:^5.1.5" - bn.js: "npm:^5.2.1" - tslib: "npm:^2.6.2" - checksum: 10c0/e426d31f8a6b8e8c57b86c18b419312906c5a169e5b2d89c15b54a5d6cf297912250d336f81926e07511ce825d36222d9e6387a01240aa6a20b11aa25dc8226a - languageName: node - linkType: hard - "@polkadot/util@npm:13.0.2, @polkadot/util@npm:^13.0.0, @polkadot/util@npm:^13.0.2": version: 13.0.2 resolution: "@polkadot/util@npm:13.0.2" @@ -2826,16 +2782,6 @@ __metadata: languageName: node linkType: hard -"@polkadot/x-bigint@npm:12.6.2": - version: 12.6.2 - resolution: "@polkadot/x-bigint@npm:12.6.2" - dependencies: - "@polkadot/x-global": "npm:12.6.2" - tslib: "npm:^2.6.2" - checksum: 10c0/78123efa2a5fad7fccb79dbe0c44f5506b70405a2b9b1dc9db9450ddd2f01791b011a46c9fff31ed8b21aace6f676179c4b7746c97ca254e8822bcf543e4d779 - languageName: node - linkType: hard - "@polkadot/x-bigint@npm:13.0.2, @polkadot/x-bigint@npm:^13.0.2": version: 13.0.2 resolution: "@polkadot/x-bigint@npm:13.0.2" @@ -2857,15 +2803,6 @@ __metadata: languageName: node linkType: hard -"@polkadot/x-global@npm:12.6.2": - version: 12.6.2 - resolution: "@polkadot/x-global@npm:12.6.2" - dependencies: - tslib: "npm:^2.6.2" - checksum: 10c0/63738eb46465e3e43151d746321c178131385a734e1d3865fc76667fec9d4b1fb8b35a0d8ee75834035b54a4047e0bae86c4f2e465b16c73d4fc15ec4426446f - languageName: node - linkType: hard - "@polkadot/x-global@npm:13.0.2, @polkadot/x-global@npm:^13.0.2": version: 13.0.2 resolution: "@polkadot/x-global@npm:13.0.2" @@ -2875,19 +2812,6 @@ __metadata: languageName: node linkType: hard -"@polkadot/x-randomvalues@npm:12.6.2": - version: 12.6.2 - resolution: "@polkadot/x-randomvalues@npm:12.6.2" - dependencies: - "@polkadot/x-global": "npm:12.6.2" - tslib: "npm:^2.6.2" - peerDependencies: - "@polkadot/util": 12.6.2 - "@polkadot/wasm-util": "*" - checksum: 10c0/44920ec7a93ca0b5b0d2abae493fe5a9fb8cdb44b70029d431c1244a11dea0a9f14d216b4d14bde8b984199b9dd364a3ae68b51937784645343f686b3613c223 - languageName: node - linkType: hard - "@polkadot/x-randomvalues@npm:13.0.2": version: 13.0.2 resolution: "@polkadot/x-randomvalues@npm:13.0.2" @@ -2901,16 +2825,6 @@ __metadata: languageName: node linkType: hard -"@polkadot/x-textdecoder@npm:12.6.2": - version: 12.6.2 - resolution: "@polkadot/x-textdecoder@npm:12.6.2" - dependencies: - "@polkadot/x-global": "npm:12.6.2" - tslib: "npm:^2.6.2" - checksum: 10c0/d1aa46dc0c4f88bce3cb7aaadbede99c2fb159c0fd317fb9fe5b54bdbb83da9cce3a5d628e25892028b34cc4eeef72669c344f0af12e21f05429142cc7b4732d - languageName: node - linkType: hard - "@polkadot/x-textdecoder@npm:13.0.2": version: 13.0.2 resolution: "@polkadot/x-textdecoder@npm:13.0.2" @@ -2921,16 +2835,6 @@ __metadata: languageName: node linkType: hard -"@polkadot/x-textencoder@npm:12.6.2": - version: 12.6.2 - resolution: "@polkadot/x-textencoder@npm:12.6.2" - dependencies: - "@polkadot/x-global": "npm:12.6.2" - tslib: "npm:^2.6.2" - checksum: 10c0/fa234ce4d164991ea98f34e9eae2adf0c4d2b0806e2e30b11c41a52b432f8cbd91fb16945243809fd9433c513b8c7ab4c16d902b92faf7befaa523daae7459f4 - languageName: node - linkType: hard - "@polkadot/x-textencoder@npm:13.0.2": version: 13.0.2 resolution: "@polkadot/x-textencoder@npm:13.0.2" @@ -3027,7 +2931,7 @@ __metadata: languageName: node linkType: hard -"@substrate/ss58-registry@npm:^1.44.0, @substrate/ss58-registry@npm:^1.46.0": +"@substrate/ss58-registry@npm:^1.46.0": version: 1.49.0 resolution: "@substrate/ss58-registry@npm:1.49.0" checksum: 10c0/b50f32e2f4632b31b3e09cec026fef557b1b72f61b6811673f5b0fbe311c5394c2f19fc4c23f97b014c77eb2d0f535a8f079dfd3fb22d5a1d7b043ceeac0d9ac