diff --git a/src/keri/app/credentialing.ts b/src/keri/app/credentialing.ts index 36ce5eb0..180dcc48 100644 --- a/src/keri/app/credentialing.ts +++ b/src/keri/app/credentialing.ts @@ -191,6 +191,7 @@ export class Credentials { args: CredentialData ): Promise { const hab = await this.client.identifiers().get(name); + const events = await this.client.keyEvents().get(hab.prefix); const estOnly = hab.state.c !== undefined && hab.state.c.includes('EO'); if (estOnly) { // TODO implement rotation event @@ -230,10 +231,25 @@ export class Credentials { dt: subject.dt, }); - const sn = parseInt(hab.state.s, 16); + let sn = parseInt(hab.state.s, 16); + sn = sn + 1; + let dig = hab.state.d; + + // check if last event already has the anchor in it + // and avoid creating a new event if it does + const lastEvent = events[events.length - 1]; + if ( + lastEvent?.a?.length == 1 && + lastEvent?.a[0]?.i == iss.i && + lastEvent?.a[0]?.s == iss.s && + lastEvent?.a[0]?.d == iss.d + ) { + sn = sn - 1; // revert sn + dig = hab.state.p!; + } const anc = interact({ pre: hab.prefix, - sn: sn + 1, + sn: sn, data: [ { i: iss.i, @@ -241,7 +257,7 @@ export class Credentials { d: iss.d, }, ], - dig: hab.state.d, + dig: dig, version: undefined, kind: undefined, }); @@ -287,6 +303,7 @@ export class Credentials { datetime?: string ): Promise { const hab = await this.client.identifiers().get(name); + const events = await this.client.keyEvents().get(hab.prefix); const pre: string = hab.prefix; const vs = versify(Ident.KERI, undefined, Serials.JSON, 0); @@ -320,8 +337,9 @@ export class Credentials { var estOnly = false; } - const sn = parseInt(state.s, 16); - const dig = state.d; + let sn = parseInt(state.s, 16); + sn = sn + 1; + let dig = state.d; const data: any = [ { @@ -332,6 +350,18 @@ export class Credentials { ]; const keeper = this.client!.manager!.get(hab); + // check if last event already has the anchor in it + // and avoid creating a new event if it does + const lastEvent = events[events.length - 1]; + if ( + lastEvent?.a?.length == 1 && + lastEvent?.a[0]?.i == rev.i && + lastEvent?.a[0]?.s == rev.s && + lastEvent?.a[0]?.d == rev.d + ) { + sn = sn - 1; // revert sn + dig = state.p!; + } if (estOnly) { // TODO implement rotation event @@ -339,7 +369,7 @@ export class Credentials { } else { const serder = interact({ pre: pre, - sn: sn + 1, + sn: sn, data: data, dig: dig, version: undefined, @@ -585,6 +615,7 @@ export class Registries { nonce, }: CreateRegistryArgs): Promise { const hab = await this.client.identifiers().get(name); + const events = await this.client.keyEvents().get(hab.prefix); const pre: string = hab.prefix; const cnfg: string[] = []; @@ -604,8 +635,9 @@ export class Registries { throw new Error('establishment only not implemented'); } else { const state = hab.state; - const sn = parseInt(state.s, 16); - const dig = state.d; + let sn = parseInt(state.s, 16); + sn = sn + 1; + let dig = state.d; const data: any = [ { @@ -615,9 +647,22 @@ export class Registries { }, ]; + // check if last event already has the anchor in it + // and avoid creating a new event if it does + const lastEvent = events[events.length - 1]; + if ( + lastEvent?.a?.length == 1 && + lastEvent?.a[0]?.i == regser.pre && + lastEvent?.a[0]?.s == '0' && + lastEvent?.a[0]?.d == regser.pre + ) { + sn = sn - 1; // revert sn + dig = state.p!; + } + const serder = interact({ pre: pre, - sn: sn + 1, + sn: sn, data: data, dig: dig, version: Versionage, diff --git a/src/keri/core/manager.ts b/src/keri/core/manager.ts index dbbb8f2d..d51c0374 100644 --- a/src/keri/core/manager.ts +++ b/src/keri/core/manager.ts @@ -539,7 +539,7 @@ export class Manager { pp.stem = creator.stem; pp.tier = creator.tier; - const dt = new Date().toString(); + const dt = new Date().toISOString().replace('Z', '000+00:00'); const nw = new PubLot(); nw.pubs = Array.from(verfers, (verfer: Verfer) => verfer.qb64); nw.ridx = ridx; @@ -793,7 +793,7 @@ export class Manager { (signer: Signer) => new Diger({ code: dcode }, signer.verfer.qb64b) ); - const dt = new Date().toString(); + const dt = new Date().toISOString().replace('Z', '000+00:00'); ps.nxt = new PubLot(); ps.nxt.pubs = Array.from( keys.signers, diff --git a/test/app/credentialing.test.ts b/test/app/credentialing.test.ts index 53d64243..c14cdd45 100644 --- a/test/app/credentialing.test.ts +++ b/test/app/credentialing.test.ts @@ -135,6 +135,67 @@ const mockGetAID = { windexes: [], }; +const mockGetAID2 = { + name: 'aid2', + prefix: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + salty: { + sxlt: '1AAHnNQTkD0yxOC9tSz_ukbB2e-qhDTStH18uCsi5PCwOyXLONDR3MeKwWv_AVJKGKGi6xiBQH25_R1RXLS2OuK3TN3ovoUKH7-A', + pidx: 0, + kidx: 0, + stem: 'signify:aid', + tier: 'low', + dcode: 'E', + icodes: ['A'], + ncodes: ['A'], + transferable: true, + }, + transferable: true, + state: { + vn: [1, 0], + i: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + s: '1', + p: '', + d: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + f: '0', + dt: '2023-08-21T22:30:46.473545+00:00', + et: 'ixn', + kt: '1', + k: ['DPmhSfdhCPxr3EqjxzEtF8TVy0YX7ATo0Uc8oo2cnmY9'], + nt: '1', + n: ['EAORnRtObOgNiOlMolji-KijC_isa3lRDpHCsol79cOc'], + bt: '0', + b: [], + c: [], + ee: { + s: '0', + d: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + br: [], + ba: [], + }, + di: '', + }, + windexes: [], +}; + +const mockEvents = [ + { + i: 'a prefix', + s: '0', + }, + { + i: 'a prefix', + s: '1', + d: 'a 2nd digest', + a: [ + { + d: 'EGB8Q3UgcsLftRsffimiRS0pqpVgRNr4Di8qorKm0u1_', + i: 'EK_6Rlxmdl6Ieyd5oz81HF3Kvv2E8nCG1rYRHA7CZPRF', + s: '0', + }, + ], + }, +]; + const mockCredential = { sad: { v: 'ACDC10JSON000197_', @@ -210,9 +271,19 @@ fetchMock.mockResponse((req) => { req.method, requrl.pathname.split('?')[0] ); - const body = req.url.startsWith(url + '/credentials') - ? mockCredential - : mockGetAID; + + let body = {}; + if (req.url.startsWith(url + '/credentials')) { + body = mockCredential; + } else if (req.url === url + '/identifiers/aid1') { + body = mockGetAID; + } else if (req.url === url + '/identifiers/aid2') { + body = mockGetAID2; + } else if (req.url.startsWith(url + '/events')) { + body = mockEvents; + } else if (req.url.startsWith(url + '/identifiers')) { + body = mockGetAID; + } return Promise.resolve({ body: JSON.stringify(body), @@ -369,6 +440,40 @@ describe('Credentialing', () => { 'EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX' ); }); + it('Issue credentials when anchor is already in the KEL', async () => { + await libsodium.ready; + const bran = '0123456789abcdefghijk'; + + const client = new SignifyClient(url, bran, Tier.low, boot_url); + + await client.boot(); + await client.connect(); + + const credentials = client.credentials(); + + const registry = 'EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX'; + const schema = 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao'; + const isuee = 'EG2XjQN-3jPN5rcR4spLjaJyM4zA6Lgg-Hd5vSMymu5p'; + await credentials.issue('aid2', { + ri: registry, + s: schema, + a: { + i: isuee, + LEI: '1234', + dt: '2023-08-23T15:16:07.553000+00:00', + }, + }); + const lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; + const lastBody = JSON.parse(lastCall[1]!.body!.toString()); + assert.equal(lastCall[0]!, url + '/identifiers/aid2/credentials'); + assert.equal(lastCall[1]!.method, 'POST'); + assert.equal(lastBody.iss.s, '0'); + assert.equal(lastBody.iss.t, 'iss'); + assert.equal(lastBody.iss.ri, registry); + assert.equal(lastBody.iss.i, lastBody.acdc.d); + assert.equal(lastBody.ixn.t, 'ixn'); + assert.equal(lastBody.ixn.s, '1'); + }); }); describe('Ipex', () => { diff --git a/test/app/registry.test.ts b/test/app/registry.test.ts index 3e72355b..43c8ec8a 100644 --- a/test/app/registry.test.ts +++ b/test/app/registry.test.ts @@ -3,7 +3,7 @@ import { anyOfClass, anything, instance, mock, when } from 'ts-mockito'; import libsodium from 'libsodium-wrappers-sumo'; import 'whatwg-fetch'; import { Registries } from '../../src/keri/app/credentialing'; -import { Identifier, KeyManager, SaltyKeeper } from '../../src'; +import { Identifier, KeyEvents, KeyManager, SaltyKeeper } from '../../src'; import { strict as assert } from 'assert'; import { HabState, State } from '../../src/keri/core/state'; @@ -14,11 +14,13 @@ describe('registry', () => { const mockedIdentifiers = mock(Identifier); const mockedKeyManager = mock(KeyManager); const mockedKeeper = mock(SaltyKeeper); + const mockedKeyEvents = mock(KeyEvents); const hab = { prefix: 'hab prefix', state: { s: '0', d: 'a digest' } as State, } as HabState; + const events: never[] = []; when(mockedClient.manager).thenReturn(instance(mockedKeyManager)); when(mockedKeyManager.get(hab)).thenReturn(instance(mockedKeeper)); @@ -31,6 +33,8 @@ describe('registry', () => { when(mockedClient.identifiers()).thenReturn( instance(mockedIdentifiers) ); + when(mockedKeyEvents.get(hab.prefix)).thenResolve(events); + when(mockedClient.keyEvents()).thenReturn(instance(mockedKeyEvents)); const mockedResponse = mock(Response); when( @@ -63,6 +67,7 @@ describe('registry', () => { await libsodium.ready; const mockedClient = mock(SignifyClient); const mockedIdentifiers = mock(Identifier); + const mockedKeyEvents = mock(KeyEvents); const hab = { prefix: 'hab prefix', @@ -72,10 +77,18 @@ describe('registry', () => { windexes: [], } as HabState; + const events = [ + { + a: [], + }, + ]; + when(mockedIdentifiers.get('a name')).thenResolve(hab); when(mockedClient.identifiers()).thenReturn( instance(mockedIdentifiers) ); + when(mockedKeyEvents.get(hab.prefix)).thenResolve(events); + when(mockedClient.keyEvents()).thenReturn(instance(mockedKeyEvents)); const registries = new Registries(instance(mockedClient)); @@ -93,4 +106,81 @@ describe('registry', () => { } ); }); + + it('should create a registry when the anchor is already on the KEL', async () => { + await libsodium.ready; + const mockedClient = mock(SignifyClient); + const mockedIdentifiers = mock(Identifier); + const mockedKeyManager = mock(KeyManager); + const mockedKeeper = mock(SaltyKeeper); + const mockedKeyEvents = mock(KeyEvents); + + const hab = { + prefix: 'hab prefix', + state: { + i: 'a prefix', + s: '1', + d: 'a digest', + p: 'old digest', + } as State, + } as HabState; + const events = [ + { + i: 'a prefix', + s: '0', + }, + { + i: 'a prefix', + s: '1', + d: 'a 2nd digest', + a: [ + { + i: 'EMppKX_JxXBuL_xE3A_a6lOcseYwaB7jAvZ0YFdgecXX', + s: '0', + d: 'EMppKX_JxXBuL_xE3A_a6lOcseYwaB7jAvZ0YFdgecXX', + }, + ], + }, + ]; + + when(mockedClient.manager).thenReturn(instance(mockedKeyManager)); + when(mockedKeyManager.get(hab)).thenReturn(instance(mockedKeeper)); + + when(mockedKeeper.sign(anyOfClass(Uint8Array))).thenResolve([ + 'a signature', + ]); + + when(mockedIdentifiers.get('a name')).thenResolve(hab); + when(mockedClient.identifiers()).thenReturn( + instance(mockedIdentifiers) + ); + when(mockedKeyEvents.get(hab.prefix)).thenResolve(events); + when(mockedClient.keyEvents()).thenReturn(instance(mockedKeyEvents)); + + const mockedResponse = mock(Response); + when( + mockedClient.fetch( + '/identifiers/a name/registries', + 'POST', + anything() + ) + ).thenResolve(instance(mockedResponse)); + + const registries = new Registries(instance(mockedClient)); + + const actual = await registries.create({ + name: 'a name', + registryName: 'a registry name', + nonce: '', + }); + + assert.equal( + actual.regser.raw, + '{"v":"KERI10JSON0000c5_","t":"vcp","d":"EMppKX_JxXBuL_xE3A_a6lOcseYwaB7jAvZ0YFdgecXX","i":"EMppKX_JxXBuL_xE3A_a6lOcseYwaB7jAvZ0YFdgecXX","ii":"hab prefix","s":"0","c":["NB"],"bt":"0","b":[],"n":""}' + ); + assert.equal( + actual.serder.raw, + '{"v":"KERI10JSON0000f6_","t":"ixn","d":"EN9qu43BedCpLgKeeb01FfZa5T50mWNnpIdQEHF4k799","i":"hab prefix","s":"1","p":"old digest","a":[{"i":"EMppKX_JxXBuL_xE3A_a6lOcseYwaB7jAvZ0YFdgecXX","s":"0","d":"EMppKX_JxXBuL_xE3A_a6lOcseYwaB7jAvZ0YFdgecXX"}]}' + ); + }); });