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