mirror of
https://github.com/tompro/sattle.git
synced 2026-08-27 07:15:59 +00:00
feat: validate trusted mint registry envelopes
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
// Hostile backup merge policy stays separate from live mint transitions:
|
||||
// local records win, and imported trust starts unlocked and unconfirmed.
|
||||
|
||||
import type {TrustedMint} from './trustedMints'
|
||||
import {isJsonObject} from './jsonParsing'
|
||||
import {isValidMintPubkey, type MintTransition} from './trustedMintTransitions'
|
||||
|
||||
export const mergeMints = (mints: TrustedMint[], incoming: unknown[]): MintTransition<number> => {
|
||||
const knownServers = new Set(mints.map((mint) => mint.server))
|
||||
const merged = [...mints]
|
||||
let added = 0
|
||||
for (const mint of incoming) {
|
||||
if (
|
||||
!isJsonObject(mint) ||
|
||||
typeof mint.server !== 'string' ||
|
||||
typeof mint.mintPubkey !== 'string' ||
|
||||
typeof mint.addedAt !== 'number' ||
|
||||
!isValidMintPubkey(mint.mintPubkey)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (knownServers.has(mint.server)) continue
|
||||
merged.push({
|
||||
server: mint.server,
|
||||
mintPubkey: mint.mintPubkey.toLowerCase(),
|
||||
addedAt: mint.addedAt,
|
||||
locked: false,
|
||||
unconfirmed: true,
|
||||
nodeAlias: typeof mint.nodeAlias === 'string' ? mint.nodeAlias : undefined,
|
||||
nodeColor: typeof mint.nodeColor === 'string' ? mint.nodeColor : undefined,
|
||||
nodeCapacityMsat:
|
||||
typeof mint.nodeCapacityMsat === 'number' ? mint.nodeCapacityMsat : undefined,
|
||||
nodeNumChannels: typeof mint.nodeNumChannels === 'number' ? mint.nodeNumChannels : undefined,
|
||||
nodeNumPeers: typeof mint.nodeNumPeers === 'number' ? mint.nodeNumPeers : undefined,
|
||||
username: typeof mint.username === 'string' ? mint.username : undefined,
|
||||
})
|
||||
knownServers.add(mint.server)
|
||||
added++
|
||||
}
|
||||
return added === 0
|
||||
? {mints, result: 0, changed: false}
|
||||
: {mints: merged, result: added, changed: true}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Strict trusted-mint envelope parsing is shared by localStorage and the
|
||||
// durable cross-context commit mirror. Either source fails closed as a whole.
|
||||
|
||||
import {isWalletOwnerId} from './storage/walletOwner'
|
||||
import type {TrustedMint} from './trustedMints'
|
||||
import {isValidMintPubkey} from './trustedMintTransitions'
|
||||
|
||||
export const TRUSTED_MINTS_REGISTRY_VERSION = 1
|
||||
|
||||
export type TrustedMintsRegistryEnvelope = {
|
||||
readonly version: typeof TRUSTED_MINTS_REGISTRY_VERSION
|
||||
readonly ownerId: string
|
||||
readonly mints: TrustedMint[]
|
||||
}
|
||||
|
||||
export type StoredTrustedMintsRegistry =
|
||||
| {readonly kind: 'absent'}
|
||||
| {readonly kind: 'malformed'}
|
||||
| {readonly kind: 'valid'; readonly envelope: TrustedMintsRegistryEnvelope}
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === 'object' && value !== null
|
||||
|
||||
const isOptionalString = (value: unknown): boolean =>
|
||||
value === undefined || typeof value === 'string'
|
||||
|
||||
const isOptionalNumber = (value: unknown): boolean =>
|
||||
value === undefined || typeof value === 'number'
|
||||
|
||||
const isTrustedMint = (value: unknown): value is TrustedMint => {
|
||||
if (!isRecord(value)) return false
|
||||
return (
|
||||
typeof value.server === 'string' &&
|
||||
typeof value.mintPubkey === 'string' &&
|
||||
isValidMintPubkey(value.mintPubkey) &&
|
||||
typeof value.addedAt === 'number' &&
|
||||
typeof value.locked === 'boolean' &&
|
||||
(value.unconfirmed === undefined || typeof value.unconfirmed === 'boolean') &&
|
||||
(value.pendingMintPubkey === undefined ||
|
||||
(typeof value.pendingMintPubkey === 'string' &&
|
||||
isValidMintPubkey(value.pendingMintPubkey))) &&
|
||||
isOptionalString(value.nodeAlias) &&
|
||||
isOptionalString(value.nodeColor) &&
|
||||
isOptionalNumber(value.nodeCapacityMsat) &&
|
||||
isOptionalNumber(value.nodeNumChannels) &&
|
||||
isOptionalNumber(value.nodeNumPeers) &&
|
||||
isOptionalString(value.username)
|
||||
)
|
||||
}
|
||||
|
||||
export const parseStoredTrustedMintsRegistry = (raw: string | null): StoredTrustedMintsRegistry => {
|
||||
if (raw === null) return {kind: 'absent'}
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(raw)
|
||||
if (
|
||||
!isRecord(parsed) ||
|
||||
parsed.version !== TRUSTED_MINTS_REGISTRY_VERSION ||
|
||||
!isWalletOwnerId(parsed.ownerId) ||
|
||||
!Array.isArray(parsed.mints) ||
|
||||
!parsed.mints.every(isTrustedMint)
|
||||
) {
|
||||
return {kind: 'malformed'}
|
||||
}
|
||||
return {
|
||||
kind: 'valid',
|
||||
envelope: {
|
||||
version: TRUSTED_MINTS_REGISTRY_VERSION,
|
||||
ownerId: parsed.ownerId,
|
||||
mints: parsed.mints,
|
||||
},
|
||||
}
|
||||
} catch {
|
||||
return {kind: 'malformed'}
|
||||
}
|
||||
}
|
||||
|
||||
export const parseLegacyTrustedMintsRegistry = (raw: string | null): TrustedMint[] | null => {
|
||||
if (raw === null) return null
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(raw)
|
||||
return Array.isArray(parsed) && parsed.every(isTrustedMint) ? parsed : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const serializeTrustedMintsRegistry = (ownerId: string, mints: TrustedMint[]): string =>
|
||||
JSON.stringify({version: TRUSTED_MINTS_REGISTRY_VERSION, ownerId, mints})
|
||||
Reference in New Issue
Block a user