-
Notifications
You must be signed in to change notification settings - Fork 123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add republish record to ipns #745
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -257,7 +257,7 @@ | |
|
||
import { NotFoundError, isPublicKey } from '@libp2p/interface' | ||
import { logger } from '@libp2p/logger' | ||
import { createIPNSRecord, marshalIPNSRecord, multihashToIPNSRoutingKey, unmarshalIPNSRecord, type IPNSRecord } from 'ipns' | ||
import { createIPNSRecord, extractPublicKeyFromIPNSRecord, marshalIPNSRecord, multihashToIPNSRoutingKey, unmarshalIPNSRecord, type IPNSRecord } from 'ipns' | ||
import { ipnsSelector } from 'ipns/selector' | ||
import { ipnsValidator } from 'ipns/validator' | ||
import { base36 } from 'multiformats/bases/base36' | ||
|
@@ -379,6 +379,13 @@ export interface RepublishOptions extends AbortOptions, ProgressOptions<Republis | |
interval?: number | ||
} | ||
|
||
export interface RepublishRecordOptions extends AbortOptions, ProgressOptions<PublishProgressEvents | IPNSRoutingEvents> { | ||
/** | ||
* Only publish to a local datastore (default: false) | ||
*/ | ||
offline?: boolean | ||
} | ||
|
||
export interface ResolveResult { | ||
/** | ||
* The CID that was resolved | ||
|
@@ -430,6 +437,13 @@ export interface IPNS { | |
* Periodically republish all IPNS records found in the datastore | ||
*/ | ||
republish(options?: RepublishOptions): void | ||
|
||
/** | ||
* Republish an existing IPNS record without the private key | ||
* | ||
* The public key is optional if the record has an embedded public key. | ||
*/ | ||
republishRecord(record: IPNSRecord, pubKey?: PublicKey, options?: RepublishRecordOptions): Promise<void> | ||
} | ||
|
||
export type { IPNSRouting } from './routing/index.js' | ||
|
@@ -707,6 +721,33 @@ class DefaultIPNS implements IPNS { | |
|
||
return unmarshalIPNSRecord(record) | ||
} | ||
|
||
async republishRecord (record: IPNSRecord, pubKey?: PublicKey, options: RepublishRecordOptions = {}): Promise<void> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto |
||
try { | ||
let mh = extractPublicKeyFromIPNSRecord(record)?.toMultihash() // try to extract the public key from the record | ||
if (mh == null) { | ||
// if no public key is provided, use the pubKey that was passed in | ||
mh = pubKey?.toMultihash() | ||
} | ||
|
||
if (mh == null) { | ||
throw new Error('No public key found to determine the routing key') | ||
} | ||
|
||
const routingKey = multihashToIPNSRoutingKey(mh) | ||
const marshaledRecord = marshalIPNSRecord(record) | ||
|
||
await this.localStore.put(routingKey, marshaledRecord, options) | ||
|
||
if (options.offline !== true) { | ||
// publish record to routing | ||
await Promise.all(this.routers.map(async r => { await r.put(routingKey, marshaledRecord, options) })) | ||
} | ||
} catch (err: any) { | ||
options.onProgress?.(new CustomProgressEvent<Error>('ipns:publish:error', err)) | ||
throw err | ||
} | ||
} | ||
} | ||
|
||
export interface IPNSOptions { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
/* eslint-env mocha */ | ||
|
||
import { generateKeyPair } from '@libp2p/crypto/keys' | ||
import { defaultLogger } from '@libp2p/logger' | ||
import { expect } from 'aegir/chai' | ||
import { MemoryDatastore } from 'datastore-core' | ||
import { createIPNSRecord } from 'ipns' | ||
import { CID } from 'multiformats/cid' | ||
import { stubInterface } from 'sinon-ts' | ||
import { ipns } from '../src/index.js' | ||
import type { IPNS, IPNSRecord, IPNSRouting } from '../src/index.js' | ||
import type { Routing } from '@helia/interface' | ||
import type { PrivateKey } from '@libp2p/interface' | ||
import type { DNS } from '@multiformats/dns' | ||
import type { StubbedInstance } from 'sinon-ts' | ||
|
||
describe('republishRecord', () => { | ||
let testCid: CID | ||
let rsaKey: PrivateKey | ||
let rsaRecord: IPNSRecord | ||
let ed25519Key: PrivateKey | ||
let ed25519Record: IPNSRecord | ||
let name: IPNS | ||
let customRouting: StubbedInstance<IPNSRouting> | ||
let heliaRouting: StubbedInstance<Routing> | ||
let dns: StubbedInstance<DNS> | ||
|
||
beforeEach(async () => { | ||
const datastore = new MemoryDatastore() | ||
customRouting = stubInterface<IPNSRouting>() | ||
customRouting.get.throws(new Error('Not found')) | ||
heliaRouting = stubInterface<Routing>() | ||
|
||
name = ipns( | ||
{ | ||
datastore, | ||
routing: heliaRouting, | ||
dns, | ||
logger: defaultLogger() | ||
}, | ||
{ | ||
routers: [customRouting] | ||
} | ||
) | ||
|
||
testCid = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn') | ||
rsaKey = await generateKeyPair('RSA') // RSA will embed the public key in the record | ||
ed25519Key = await generateKeyPair('Ed25519') | ||
rsaRecord = await createIPNSRecord(rsaKey, testCid, 1n, 24 * 60 * 60 * 1000) | ||
ed25519Record = await createIPNSRecord(ed25519Key, testCid, 1n, 24 * 60 * 60 * 1000) | ||
Comment on lines
+46
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we need to create all of these before each test? could we create them in the tests that use them? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just a nit here, but testCid could probably be moved up into the describe block, and then the records and keys could be moved into individual tests |
||
}) | ||
|
||
it('should republish a record using embedded public key', async () => { | ||
await expect(name.republishRecord(rsaRecord)).to.not.be.rejected | ||
}) | ||
|
||
it('should republish a record using provided public key', async () => { | ||
await expect(name.republishRecord(ed25519Record, ed25519Key.publicKey)).to.not.be.rejected | ||
}) | ||
|
||
it('should fail when no public key is available', async () => { | ||
await expect(name.republishRecord(ed25519Record)).to.be.rejectedWith( | ||
'No public key found to determine the routing key' | ||
) | ||
}) | ||
|
||
it('should emit progress events on error', async () => { | ||
const events: Error[] = [] | ||
|
||
await expect( | ||
name.republishRecord(ed25519Record, undefined, { | ||
onProgress: (evt) => { | ||
if (evt.type === 'ipns:publish:error') { | ||
events.push(evt.detail) | ||
} | ||
} | ||
}) | ||
).to.be.rejected | ||
|
||
expect(events).to.have.lengthOf(1) | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
stick pubKey into
RepublishRecordOptions
object?