Skip to content
This repository has been archived by the owner on Sep 14, 2023. It is now read-only.

chore: misc ss58 cleanup #539

Merged
merged 5 commits into from
Feb 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion effects/extrinsic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export class SignedExtrinsic<
const senderSs58 = Z.ls(addrPrefix, this.props.sender).next(([addrPrefix, sender]) => {
switch (sender.type) {
case "Id": {
return U.ss58.encode(addrPrefix, sender.value)
return U.returnThrows<U.ss58.EncodeError>()(() => U.ss58.encode(addrPrefix, sender.value))
}
default: {
unimplemented()
Expand Down Expand Up @@ -155,6 +155,7 @@ export function extrinsicsDecoded<Client extends Z.$<rpc.Client>>(client: Client
)
}

// TODO: ensure that ss58 encoding failure is represented in `U` of to-be `ExtrinsicRune`
function $extrinsic<
Client extends Z.$<rpc.Client> = Z.$<rpc.Client>,
Rest extends [sign?: Z.$<Signer>] = [sign?: Z.$<Signer>],
Expand Down
10 changes: 10 additions & 0 deletions util/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,13 @@ export function throwIfError<T>(value: T): Exclude<T, Error> {
}
return value as Exclude<T, Error>
}

export function returnThrows<Throw>() {
return <R>(run: () => R): R | Throw => {
try {
return run()
} catch (e) {
return e as Throw
}
}
}
17 changes: 7 additions & 10 deletions util/ss58.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assertEquals, assertInstanceOf } from "../deps/std/testing/asserts.ts"
import { assertEquals, assertThrows } from "../deps/std/testing/asserts.ts"
import * as ss58 from "./ss58.ts"
import { alice } from "./test_pairs.ts"

Expand Down Expand Up @@ -32,27 +32,24 @@ for (
}

Deno.test("ss58.encode invalid public key length", () => {
assertInstanceOf(
ss58.encode(0, alice.publicKey.slice(0, 30)),
ss58.InvalidPublicKeyLengthError,
)
assertThrows(() => ss58.encode(0, alice.publicKey.slice(0, 30)), ss58.InvalidPublicKeyLengthError)
})

Deno.test("ss58.encode invalid network prefix", () => {
assertInstanceOf(ss58.encode(46, alice.publicKey, [0]), ss58.InvalidNetworkPrefixError)
assertThrows(() => ss58.encode(46, alice.publicKey, [0]), ss58.InvalidNetworkPrefixError)
})

Deno.test("ss58.decodeRaw long address", () => {
assertInstanceOf(ss58.decodeRaw(new Uint8Array(40)), ss58.InvalidAddressLengthError)
assertThrows(() => ss58.decodeRaw(new Uint8Array(40)), ss58.InvalidAddressLengthError)
})

Deno.test("ss58.decodeRaw short address", () => {
assertInstanceOf(ss58.decodeRaw(new Uint8Array(30)), ss58.InvalidAddressLengthError)
assertThrows(() => ss58.decodeRaw(new Uint8Array(30)), ss58.InvalidAddressLengthError)
})

Deno.test("ss58.decodeRaw invalid checksum", () => {
assertInstanceOf(
ss58.decodeRaw(Uint8Array.of(0, ...alice.publicKey, 255, 255)),
assertThrows(
() => ss58.decodeRaw(Uint8Array.of(0, ...alice.publicKey, 255, 255)),
ss58.InvalidAddressChecksumError,
)
})
79 changes: 36 additions & 43 deletions util/ss58.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,27 @@
import * as base58 from "../deps/std/encoding/base58.ts"
import { Blake2b } from "../deps/wat_the_crypto.ts"

// SS58PRE string (0x53533538505245 hex) encoded as Uint8Array
const SS58PRE = Uint8Array.of(83, 83, 53, 56, 80, 82, 69)
const CHECKSUM_LENGTH = 2
const VALID_ADDRESS_LENGTHS: Record<number, boolean | undefined> = {
35: true,
36: true,
37: true,
38: true,
}
const VALID_PUBLIC_KEY_LENGTHS: Record<number, boolean | undefined> = {
32: true,
33: true,
}

export const encode = (
export function encode(
prefix: number,
pubKey: Uint8Array,
validNetworkPrefixes?: readonly number[],
) => {
const encodeRawResult = encodeRaw(prefix, pubKey, validNetworkPrefixes)
if (encodeRawResult instanceof Error) return encodeRawResult
return base58.encode(encodeRawResult)
): string {
return base58.encode(encodeRaw(prefix, pubKey, validNetworkPrefixes))
}
export const encodeRaw = (

export function encodeRaw(
prefix: number,
pubKey: Uint8Array,
validNetworkPrefixes?: readonly number[],
): Uint8Array | InvalidPublicKeyLengthError | InvalidNetworkPrefixError => {
): Uint8Array {
const isValidPublicKeyLength = !!VALID_PUBLIC_KEY_LENGTHS[pubKey.length]

if (!isValidPublicKeyLength) {
return new InvalidPublicKeyLengthError()
throw new InvalidPublicKeyLengthError()
}

const isValidNetworkPrefix = !validNetworkPrefixes || validNetworkPrefixes.includes(prefix)

if (!isValidNetworkPrefix) {
return new InvalidNetworkPrefixError()
}
if (!isValidNetworkPrefix) throw new InvalidNetworkPrefixError()

const prefixBytes = prefix < 64
? Uint8Array.of(prefix)
Expand All @@ -66,26 +48,24 @@ export const encodeRaw = (

return address
}

export type EncodeError = InvalidPublicKeyLengthError | InvalidNetworkPrefixError
export class InvalidPublicKeyLengthError extends Error {
override readonly name = "InvalidPublicKeyLengthError"
}
export class InvalidNetworkPrefixError extends Error {
override readonly name = "InvalidNetworkPrefixError"
}

export const decode = (address: string) => decodeRaw(base58.decode(address))
export const decodeRaw = (
address: Uint8Array,
):
| [prefix: number, pubKey: Uint8Array]
| InvalidAddressLengthError
| InvalidAddressChecksumError =>
{
const isValidAddressLength = !!VALID_ADDRESS_LENGTHS[address.length]
export type DecodeResult = [prefix: number, pubKey: Uint8Array]

if (!isValidAddressLength) {
return new InvalidAddressLengthError()
}
export function decode(address: string): DecodeResult {
return decodeRaw(base58.decode(address))
}

export function decodeRaw(address: Uint8Array): DecodeResult {
const isValidAddressLength = !!VALID_ADDRESS_LENGTHS[address.length]
if (!isValidAddressLength) throw new InvalidAddressLengthError()

const prefixLength = address[0]! & 0b0100_0000 ? 2 : 1

Expand All @@ -104,19 +84,32 @@ export const decodeRaw = (
hasher.dispose()

if (digest[0] !== checksum[0] || digest[1] !== checksum[1]) {
return new InvalidAddressChecksumError()
throw new InvalidAddressChecksumError()
}

const pubKey = address.subarray(
prefixLength,
address.length - CHECKSUM_LENGTH,
)
const pubKey = address.subarray(prefixLength, address.length - CHECKSUM_LENGTH)

return [prefix, pubKey]
}

export type DecodeError = InvalidAddressLengthError | InvalidAddressChecksumError
export class InvalidAddressLengthError extends Error {
override readonly name = "InvalidAddressError"
}
export class InvalidAddressChecksumError extends Error {
override readonly name = "InvalidAddressChecksumError"
}

// SS58PRE string (0x53533538505245 hex) encoded as Uint8Array
const SS58PRE = Uint8Array.of(83, 83, 53, 56, 80, 82, 69)
const CHECKSUM_LENGTH = 2
const VALID_ADDRESS_LENGTHS: Record<number, boolean | undefined> = {
35: true,
36: true,
37: true,
38: true,
}
const VALID_PUBLIC_KEY_LENGTHS: Record<number, boolean | undefined> = {
32: true,
33: true,
}