Oddbean new post about | logout
 Is the code open somewhere?  
 😅 the code that does the sending is a complete mess and has a non-public info in it. I’ll try to clean it up and open the source for it. Need to parameterize some of the non-public (but not a high value secret or anything) info. 
 t-y Fishcake 
 This took me way longer than I would have liked, but here is the working Typescript code that makes those god damn GW DMs.

// Create DM event
export async function creatNIP17DMEvent(
  fromSk: string,
  toNpub: string,
  dm: string[],
): Promise<Array<Event>> {
  const skBuff = hexToBytes(fromSk)
  const events: Array<Event> = []

  while (dm.length > 0) {
    const msg = dm.shift()
    if (msg)
      events.push(createNip17Wraps(msg, toNpub, skBuff))
  }

  return events
}

function getNow(): number {
  return Math.floor(Date.now() / 1000)
}

function getRandomNow(): number {
  return getNow() - Math.floor(Math.random() * 172800)
}

function nip44Encrypt(rumor: Rumor, privateKey: Uint8Array, publicKey: string): string {
  const key = nip44.v2.utils.getConversationKey(privateKey, publicKey)
  return nip44.v2.encrypt(JSON.stringify(rumor), key)
}

function createWrap(event: Event, recipientPublicKey: string): Event {
  // Get random secret key to wrap the event
  const randomSK = generateSecretKey()
  return finalizeEvent({
    kind: 1059, // Gift wrapped event
    content: nip44Encrypt(event, randomSK, recipientPublicKey),
    created_at: getRandomNow(),
    tags: [["p", recipientPublicKey]]
  }, randomSK) as Event
}

function createSeal(event: Rumor, privateKey: Uint8Array, recipientPublicKey: string): Event {
  return finalizeEvent({
    kind: 13, // Seal
    content: nip44Encrypt(event, privateKey, recipientPublicKey),
    created_at: getRandomNow(),
    tags: []
  }, privateKey)
}

function createRumor(message: string, senderPubkey: string, receiverPubkey: string): Rumor {
  const rumor = {
    kind: 14, // As per specs, should be Kind 14 event
    content: message,
    pubkey: senderPubkey,
    created_at: getNow(),
    tags: [["p", receiverPubkey]],
  } as Rumor
  rumor.id = getEventHash(rumor)
  return rumor
}

function createNip17Wraps(message: string, recipientPublicKey: string, senderSk: Uint8Array): Event {
  const senderPubkey = getPublicKey(senderSk)
  return createWrap(
    createSeal(
      createRumor(message, senderPubkey, recipientPublicKey),
      senderSk, recipientPublicKey),
    recipientPublicKey)
} 
 One fix for types:

function nip44Encrypt(rumor: Rumor | VerifiedEvent, privateKey: Uint8Array, publicKey: string): string {
  const key = nip44.v2.utils.getConversationKey(privateKey, publicKey)
  return nip44.v2.encrypt(JSON.stringify(rumor), key)
}


It does not have any impact, just cosmetics 
 It works! :)  
 Yeah! I do suggest to make a clear documentation about how it should be done, I will contribute if needed. It is super confusing now and no single NIP to look at 🫂😭 
 Maybe send a PR with some of this code to nostr-tools?  
 Good idea! 🫂 
 I should add nip40 expiration to the wrap too I think, if the event is OTP 
 Great idea