diff --git a/tests/bundle/bundle-test.ts b/tests/bundle/bundle-test.ts index ca433f0c6..53a72289f 100644 --- a/tests/bundle/bundle-test.ts +++ b/tests/bundle/bundle-test.ts @@ -7,117 +7,17 @@ /// -import type { ApiPromise } from '@polkadot/api' -import type { - Did, - DidDocument, - DidUrl, - KiltAddress, - SignerInterface, - SubmittableExtrinsic, -} from '@kiltprotocol/types' - -const { kilt } = window - -const { - ConfigService, - Issuer, - Verifier, - Holder, - DidResolver, - signAndSubmitTx, - getSignersForKeypair, -} = kilt - -async function authorizeTx( - api: ApiPromise, - call: SubmittableExtrinsic, - did: string, - signer: SignerInterface, - submitter: string, - nonce = 1 -) { - let authorized = api.tx.did.submitDidCall( - { - did: did.slice(9), - call, - blockNumber: await api.query.system.number(), - submitter, - txCounter: nonce, - }, - { ed25519: new Uint8Array(64) } - ) - - const signature = await signer.sign({ data: authorized.args[0].toU8a() }) - - authorized = api.tx.did.submitDidCall(authorized.args[0].toU8a(), { - ed25519: signature, - }) +import type { KiltAddress, SignerInterface } from '@kiltprotocol/types' - return authorized -} - -async function createFullDid( - payer: SignerInterface<'Ed25519' | 'Sr25519', KiltAddress>, - keypair: { publicKey: Uint8Array; secretKey: Uint8Array } -) { - const api = ConfigService.get('api') - - const [signer] = await getSignersForKeypair({ - keypair, - type: 'Ed25519', - }) - const address = signer.id - const getSigners: ( - didDocument: DidDocument - ) => Array> = (didDocument) => { - return ( - didDocument.verificationMethod?.map< - Array> - >(({ id }) => [ - { - ...signer, - id, - }, - ]) ?? [] - ).flat() - } +const { kilt: Kilt } = window - let tx = api.tx.did.create( - { - did: address, - submitter: payer.id, - newAttestationKey: { ed25519: keypair.publicKey }, - }, - { ed25519: new Uint8Array(64) } - ) - - const signature = await signer.sign({ data: tx.args[0].toU8a() }) - tx = api.tx.did.create(tx.args[0].toU8a(), { ed25519: signature }) - - await signAndSubmitTx(tx, payer) - - const { didDocument } = await DidResolver.resolve( - `did:kilt:${address}` as Did, - {} - ) - if (!didDocument) { - throw new Error(`failed to create did for account ${address}`) - } +async function runAll() { + const api = await Kilt.connect('ws://127.0.0.1:9944') - return { - didDocument, - getSigners, - address, - } -} + console.log('connected') -async function runAll() { - // init sdk kilt config and connect to chain - const api = await kilt.connect('ws://127.0.0.1:9944') + const authenticationKeyPair = Kilt.generateKeypair({ type: 'ed25519' }) - // Accounts - console.log('Account setup started') const faucet = { publicKey: new Uint8Array([ 238, 93, 102, 137, 215, 142, 38, 187, 91, 53, 176, 68, 23, 64, 160, 101, @@ -129,214 +29,344 @@ async function runAll() { 3, ]), } - const [payerSigner] = (await getSignersForKeypair({ + + const [submitter] = (await Kilt.getSignersForKeypair({ keypair: faucet, type: 'Ed25519', })) as Array> - console.log('faucet signer created') + console.log('keypair generation complete') + + // ┏━━━━━━━━━━━━┓ + // ┃ create DID ┃ + // ┗━━━━━━━━━━━━┛ + // + // Generate the DID-signed creation tx and submit it to the blockchain with the specified account. + // The DID Document will have one Verification Key with an authentication relationship. + // + // Note the following parameters: + // - `api`: The connected blockchain api. + // - `signers`: The keys for verification materials inside the DID Document. For creating a DID, + // only the key for the authentication verification method is required. + // - `submitter`: The account used to submit the transaction to the blockchain. Note: the submitter account must have + // enough funds to cover the required storage deposit. + // - `fromPublicKey`: The public key that will feature as the DID's initial authentication method and will determine the DID identifier. + + const transactionHandler = Kilt.DidHelpers.createDid({ + api, + signers: [authenticationKeyPair], + submitter, + fromPublicKey: authenticationKeyPair.publicKeyMultibase, + }) - const { didDocument: alice, getSigners: aliceSign } = await createFullDid( - payerSigner, - { - publicKey: new Uint8Array([ - 136, 220, 52, 23, 213, 5, 142, 196, 180, 80, 62, 12, 18, 234, 26, 10, - 137, 190, 32, 15, 233, 137, 34, 66, 61, 67, 52, 1, 79, 166, 176, 238, - ]), - secretKey: new Uint8Array([ - 171, 248, 229, 189, 190, 48, 198, 86, 86, 192, 163, 203, 209, 129, 255, - 138, 86, 41, 74, 105, 223, 237, 210, 121, 130, 170, 206, 74, 118, 144, - 145, 21, - ]), - } - ) - console.log('alice setup done') + // The `createDid` function returns a transaction handler, which includes two methods: + // - `submit`: Submits a transaction for inclusion in a block, resulting in its execution in the blockchain runtime. + // - `getSubmittable`: Produces transaction that can be submitted to a blockchain node for inclusion, or signed and submitted by an external service. - const { didDocument: bob, getSigners: bobSign } = await createFullDid( - payerSigner, - { - publicKey: new Uint8Array([ - 209, 124, 45, 120, 35, 235, 242, 96, 253, 19, 143, 45, 126, 39, 209, 20, - 192, 20, 93, 150, 139, 95, 245, 0, 97, 37, 242, 65, 79, 173, 174, 105, - ]), - secretKey: new Uint8Array([ - 59, 123, 96, 175, 42, 188, 213, 123, 164, 1, 171, 57, 143, 132, 244, - 202, 84, 189, 107, 33, 64, 210, 80, 63, 188, 243, 40, 101, 53, 254, 63, - 241, - ]), - } - ) + // Submit transaction. + // Note: `submit()` by default, waits for the block to be finalized. This behaviour can be overwritten + // in the function's optional parameters. + const didDocumentTransactionResult = await transactionHandler.submit() - console.log('bob setup done') + // Once the transaction is submitted, the result should be checked. + // For the sake of this example, we will only check if the transaction went through. + if (didDocumentTransactionResult.status !== 'confirmed') { + throw new Error('create DID failed') + } - // Light DID Account creation workflow - const authPublicKey = - '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + // Get the DID Document from the transaction result. + let { didDocument, signers } = didDocumentTransactionResult.asConfirmed + + console.log('Did created') + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Create Verification Method ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + // + // - `DidHelpers` include a function to add a verification methods. + // Similar to `createDid`, setting a verification method requires some parameters. + // + // - `didDocument` is the latest state of the DID Document that shall be updated. + // - `signers` includes all the keypairs included in the DID documents and necessary for the + // specified operation, in this case, the keypair of the authentication key, which is necessary to + // allow updates to the DID Document. + // - `publicKey` is the key used for the verification method. + // + // Note: setting a verification method will remove any existing method for the specified relationship. + + // TODO: use mnemonic here. + const assertionKeyPair = Kilt.generateKeypair({ + type: 'sr25519', + }) + const vmTransactionResult = await Kilt.DidHelpers.setVerificationMethod({ + api, + didDocument, + signers: [...signers, assertionKeyPair], + submitter, + publicKey: assertionKeyPair.publicKeyMultibase, + relationship: 'assertionMethod', + }).submit() + + if (vmTransactionResult.status !== 'confirmed') { + throw new Error('add verification method failed') + } + ;({ didDocument, signers } = vmTransactionResult.asConfirmed) - // const encPublicKey = - // '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' + console.log('assertion method added') - const address = api.createType('Address', authPublicKey).toString() - const resolved = await DidResolver.resolve( - `did:kilt:light:01${address}:z1Ac9CMtYCTRWjetJfJqJoV7FcPDD9nHPHDHry7t3KZmvYe1HQP1tgnBuoG3enuGaowpF8V88sCxytDPDy6ZxhW` as Did, - {} - ) - if ( - !resolved.didDocument || - resolved.didDocument?.keyAgreement?.length !== 1 - ) { - throw new Error('DID Test Unsuccessful') - } else console.info(`light DID successfully resolved`) - - // Chain DID workflow -> creation & deletion - console.log('DID workflow started') - const { - didDocument: fullDid, - getSigners, - address: didAddress, - } = await createFullDid(payerSigner, { - publicKey: new Uint8Array([ - 157, 198, 166, 93, 125, 173, 238, 122, 17, 146, 49, 238, 62, 111, 140, 45, - 26, 6, 94, 42, 60, 167, 79, 19, 142, 20, 212, 5, 130, 44, 214, 190, - ]), - secretKey: new Uint8Array([ - 252, 195, 96, 143, 203, 194, 37, 74, 205, 243, 137, 71, 234, 82, 57, 46, - 212, 14, 113, 177, 1, 241, 62, 118, 184, 230, 121, 219, 17, 45, 36, 143, - ]), - }) + // ┏━━━━━━━━━━━━━━━━━┓ + // ┃ Claim web3name ┃ + // ┗━━━━━━━━━━━━━━━━━┛ + const claimW3nTransactionResult = await Kilt.DidHelpers.claimWeb3Name({ + api, + didDocument, + submitter, + signers, + name: 'example123', + }).submit() - if ( - fullDid.authentication?.length === 1 && - fullDid.assertionMethod?.length === 1 && - fullDid.id.endsWith(didAddress) - ) { - console.info('DID matches') - } else { - throw new Error('DIDs do not match') + if (claimW3nTransactionResult.status !== 'confirmed') { + throw new Error('claim web3name failed') } - const deleteTx = await authorizeTx( - api, - api.tx.did.delete(0), - fullDid.id, - getSigners(fullDid)[0], - payerSigner.id - ) + // The didDocument now contains an `alsoKnownAs` entry. + ;({ didDocument } = claimW3nTransactionResult.asConfirmed) - await signAndSubmitTx(deleteTx, payerSigner) + console.log('w3n claimed') - const resolvedAgain = await DidResolver.resolve(fullDid.id, {}) - if (resolvedAgain.didDocumentMetadata.deactivated) { - console.info('DID successfully deleted') - } else { - throw new Error('DID was not deleted') - } + // ┏━━━━━━━━━━━━━━━━┓ + // ┃ Add a service ┃ + // ┗━━━━━━━━━━━━━━━━┛ + const addServiceTransactionResult = await Kilt.DidHelpers.addService({ + api, + submitter, + signers, + didDocument, + // TODO: change service endpoint. + service: { + id: '#my_service', + type: ['http://schema.org/EmailService'], + serviceEndpoint: ['mailto:info@kilt.io'], + }, + }).submit() - // CType workflow - console.log('CType workflow started') + if (addServiceTransactionResult.status !== 'confirmed') { + throw new Error('add service failed') + } + ;({ didDocument } = addServiceTransactionResult.asConfirmed) + + console.log('service added') + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Register a CType ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + // + // Register a credential type on chain so we can issue credentials against it. + // + // Note: + // We are registering a CType that has been created previously using functionality from the @kiltprotocol/credentials package. + // The @kiltprotocol/sdk-js package and bundle do not currently offer support for this. + // + // TODO: Decide if CType definitions are expected to be hardcoded in application logic, at least for credential issuance. + // Verifying credentials / presentations is already possible even if the CType definition is not known. + // const DriversLicenseDef = '{"$schema":"ipfs://bafybeiah66wbkhqbqn7idkostj2iqyan2tstc4tpqt65udlhimd7hcxjyq/","additionalProperties":false,"properties":{"age":{"type":"integer"},"name":{"type":"string"}},"title":"Drivers License","type":"object"}' - const cTypeStoreTx = await authorizeTx( + const createCTypeResult = await Kilt.DidHelpers.transact({ api, - api.tx.ctype.add(DriversLicenseDef), - alice.id, - aliceSign(alice)[0], - payerSigner.id - ) - - const result = await signAndSubmitTx(cTypeStoreTx, payerSigner) + didDocument, + signers, + submitter, + call: api.tx.ctype.add(DriversLicenseDef), + expectedEvents: [{ section: 'CType', method: 'CTypeCreated' }], + }).submit() + + if (createCTypeResult.status !== 'confirmed') { + throw new Error('CType creation failed') + } - const ctypeHash = result.events - ?.find((ev) => api.events.ctype.CTypeCreated.is(ev.event)) - ?.event.data[1].toHex() + // TODO: We don't have the CType id in the definition, so we need to get it from the events. + const ctypeHash = createCTypeResult.asConfirmed.events + .find((event) => api.events.ctype.CTypeCreated.is(event)) + ?.data[1].toHex() - if (!ctypeHash || !(await api.query.ctype.ctypes(ctypeHash)).isSome) { - throw new Error('storing ctype failed') + if ((await api.query.ctype.ctypes(ctypeHash)).isEmpty) { + throw new Error('CType not registered') } + // TODO: Should we at least be able to load an existing CType from the chain? const DriversLicense = JSON.parse(DriversLicenseDef) DriversLicense.$id = `kilt:ctype:${ctypeHash}` - console.info('CType successfully stored on chain') - - // Attestation workflow - console.log('Attestation workflow started') - const credentialSubject = { id: bob.id, name: 'Bob', age: 21 } - - const credential = await Issuer.createCredential({ + console.log('CType registered') + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Issue a Credential ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + // + // Create and issue a credential using our Did. + // The holder is also our Did, so we are issuing to ourselves here. + // + const unsigned = await Kilt.Issuer.createCredential({ + issuer: didDocument.id, + credentialSubject: { + id: didDocument.id, + age: 22, + name: 'Gustav', + }, cType: DriversLicense, - credentialSubject, - issuer: alice.id, }) - console.info('Credential subject conforms to CType') - - if ( - credential.credentialSubject.name !== credentialSubject.name || - credential.credentialSubject.age !== credentialSubject.age || - credential.credentialSubject.id !== bob.id - ) { - throw new Error('Claim content inside Credential mismatching') - } - - const issued = await Issuer.issue(credential, { - didDocument: alice, - signers: [...aliceSign(alice), payerSigner], - submitter: payerSigner.id, - }) - console.info('Credential issued') - - const credentialResult = await Verifier.verifyCredential({ - credential: issued, - config: { - cTypes: [DriversLicense], - }, + const credential = await Kilt.Issuer.issue(unsigned, { + didDocument, + signers: [...signers, submitter], + submitter: submitter.id, }) - if (credentialResult.verified) { - console.info('Credential proof verified') - console.info('Credential status verified') - } else { - throw new Error(`Credential failed to verify: ${credentialResult.error}`, { - cause: credentialResult, - }) - } - - const challenge = crypto.randomUUID() - - const derived = await Holder.deriveProof(issued, { - disclose: { allBut: ['/credentialSubject/name'] }, + console.log('credential issued') + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Create a Presentation ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + // + // Create a derived credential that only contains selected properties (selective disclosure), then create a credential presentation for it. + // The presentation includes a proof of ownership and is scoped to a verified and time frame to prevent unauthorized re-use. + // + const derived = await Kilt.Holder.deriveProof(credential, { + disclose: { only: ['/credentialSubject/age'] }, }) - const presentation = await Holder.createPresentation( + const presentation = await Kilt.Holder.createPresentation( [derived], { - didDocument: bob, - signers: bobSign(bob), + didDocument, + signers, }, - {}, - { - challenge, - } + { verifier: didDocument.id, validUntil: new Date(Date.now() + 100_000) } ) - console.info('Presentation created') - const presentationResult = await Verifier.verifyPresentation({ + console.log('presentation created') + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Verify a Presentation ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + // + // Verify a presentation. + // + // Verification would fail if: + // - The presentation is not signed by the holder's Did. + // - The current time is outside of the validity time frame of the presentation. + // - The verifier in the presentation does not match the one specified. + // + const { verified, error } = await Kilt.Verifier.verifyPresentation({ presentation, - verificationCriteria: { challenge }, + verificationCriteria: { + verifier: didDocument.id, + proofPurpose: 'authentication', + }, }) - if (presentationResult.verified) { - console.info('Presentation verified') - } else { - throw new Error( - [ - 'Presentation failed to verify', - ...(presentationResult.error ?? []), - ].join('\n '), - { cause: presentationResult } - ) + + if (verified !== true) { + throw new Error(`failed to verify credential: ${JSON.stringify(error)}`) + } + + console.log('presentation verified') + + // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + // ┃ Remove a Verification Method ┃ + // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + // + // Removing a verification method can be done by specifying its id. + // + // Note: + // - The provided `didDocument` must include the specified verification method. + // - The authentication verification method can not be removed. + const removeVmTransactionResult = + await Kilt.DidHelpers.removeVerificationMethod({ + api, + didDocument, + signers, + submitter, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + verificationMethodId: didDocument.assertionMethod![0], + relationship: 'assertionMethod', + }).submit() + + if (removeVmTransactionResult.status !== 'confirmed') { + throw new Error('remove verification method failed') } + ;({ didDocument } = removeVmTransactionResult.asConfirmed) + + console.log('assertion method removed') + + // ┏━━━━━━━━━━━━━━━━━━┓ + // ┃ Release web3name ┃ + // ┗━━━━━━━━━━━━━━━━━━┛ + // + // A web3name can be released from a DID and potentially claimed by another DID. + const releaseW3nTransactionResult = await Kilt.DidHelpers.releaseWeb3Name({ + api, + didDocument, + submitter, + signers, + }).submit() + + if (releaseW3nTransactionResult.status !== 'confirmed') { + throw new Error('release web3name failed') + } + ;({ didDocument } = releaseW3nTransactionResult.asConfirmed) + + console.log('w3n released') + + // ┏━━━━━━━━━━━━━━━━━━┓ + // ┃ Remove a service ┃ + // ┗━━━━━━━━━━━━━━━━━━┛ + // + // Services can be removed by specifying the service `id` + const removeServiceTransactionResult = await Kilt.DidHelpers.removeService({ + api, + submitter, + signers, + didDocument, + id: '#my_service', + }).submit() + + if (removeServiceTransactionResult.status !== 'confirmed') { + throw new Error('remove service failed') + } + ;({ didDocument } = removeServiceTransactionResult.asConfirmed) + + console.log('service removed') + + // ┏━━━━━━━━━━━━━━━━━━┓ + // ┃ Deactivate a DID ┃ + // ┗━━━━━━━━━━━━━━━━━━┛ + // + // _Permanently_ deactivate the DID, removing all verification methods and services from its document. + // Deactivating a DID cannot be undone, once a DID has been deactivated, all operations on it (including attempts at re-creation) are permanently disabled. + const deactivateDidTransactionResult = await Kilt.DidHelpers.deactivateDid({ + api, + submitter, + signers, + didDocument, + }).submit() + + if (deactivateDidTransactionResult.status !== 'confirmed') { + throw new Error('deactivate DID failed') + } + ;({ didDocument } = deactivateDidTransactionResult.asConfirmed) + + if (Array.isArray(didDocument.verificationMethod)) { + throw new Error('Did not deactivated') + } + + console.log('Did deactivated') + + // Release the connection to the blockchain. + await api.disconnect() + + console.log('disconnected') } window.runAll = runAll