mirror of
https://github.com/joinmarket-webui/jam.git
synced 2026-08-16 13:01:05 +02:00
chore: use base58 of scure lib
This commit is contained in:
parent
ffa76bfb71
commit
95ea6f566d
10 changed files with 172 additions and 152 deletions
1
package-lock.json
generated
1
package-lock.json
generated
|
|
@ -31,7 +31,6 @@
|
|||
"@tanstack/match-sorter-utils": "8.19.4",
|
||||
"@tanstack/react-query": "5.90.18",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"base58-js": "3.0.3",
|
||||
"bitcoin-address-validation": "3.0.0",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
|
|
|
|||
|
|
@ -66,7 +66,6 @@
|
|||
"@tanstack/match-sorter-utils": "8.19.4",
|
||||
"@tanstack/react-query": "5.90.18",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"base58-js": "3.0.3",
|
||||
"bitcoin-address-validation": "3.0.0",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import { useApiClient } from '@/hooks/useApiClient'
|
|||
import { deriveAccountXpub, detectNetwork } from '@/lib/bip32'
|
||||
import { hashPassword } from '@/lib/hash'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { toNativeSegwitPub } from '@/lib/xpub'
|
||||
import { convertExtendedPublicKey } from '@/lib/xpubs'
|
||||
import { authStore } from '@/store/authStore'
|
||||
import type { JarIndex } from '@/types/global'
|
||||
|
||||
|
|
@ -77,12 +77,11 @@ async function deriveAccountXpubsFromSeed(
|
|||
xpub: xpub,
|
||||
})
|
||||
} else {
|
||||
const nativeSegwitXpub = await toNativeSegwitPub(xpub)
|
||||
xpubs.push({
|
||||
name: 'zpub',
|
||||
path,
|
||||
network,
|
||||
xpub: nativeSegwitXpub,
|
||||
xpub: convertExtendedPublicKey(xpub, 'zpub'),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,24 +2,21 @@ import { HDKey } from '@scure/bip32'
|
|||
import { Network } from 'bitcoin-address-validation'
|
||||
|
||||
/**
|
||||
* Derive account-level xpub from mnemonic phrase
|
||||
* JoinMarket uses BIP84 (Native SegWit) with paths:
|
||||
* - Mainnet: m/84'/0'/account'
|
||||
* - Testnet: m/84'/1'/account'
|
||||
* Derive account-level xpub from seed
|
||||
*
|
||||
* @param seed BIP39 mnemonic phrase (12 or 24 words)
|
||||
* @param seed BIP32 seed
|
||||
* @param path HD key path (m / purpose' / coin_type' / account' / change / address_index), e.g. `m/84'/0'/0'`
|
||||
* @returns Extended public key (xpub for mainnet, tpub for testnet)
|
||||
* @returns Extended public key (xpub)
|
||||
*/
|
||||
export function deriveAccountXpub(seed: Uint8Array, path: string): string {
|
||||
const root = HDKey.fromMasterSeed(seed)
|
||||
const accountKey = root.derive(path)
|
||||
const key = root.derive(path)
|
||||
|
||||
if (!accountKey.publicExtendedKey) {
|
||||
throw new Error(`Failed to derive extended public key for path ${path}`)
|
||||
if (!key.publicExtendedKey) {
|
||||
throw new Error(`Failed to derive extended public key for path ${path}.`)
|
||||
}
|
||||
|
||||
return accountKey.publicExtendedKey
|
||||
return key.publicExtendedKey
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { OfferType } from '@/constants/jm'
|
||||
import type { Milliseconds } from '@/types/global'
|
||||
import type { Milliseconds, SeedPhrase } from '@/types/global'
|
||||
|
||||
const HORIZONTAL_ELLIPSIS = '\u2026' // Horizontal Ellipsis `…`
|
||||
|
||||
|
|
@ -82,6 +82,9 @@ export const btcToSats = (value: string) => Math.round(parseFloat(value) * 100_0
|
|||
|
||||
export const SEGWIT_ACTIVATION_BLOCK = 481_824 // https://github.com/bitcoin/bitcoin/blob/v25.0/src/kernel/chainparams.cpp#L86
|
||||
|
||||
export const DUMMY_SEED_PHRASE: SeedPhrase =
|
||||
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'.split(' ')
|
||||
|
||||
export const percentageToFactor = (val: number, precision = 6) => {
|
||||
return Number((val / 100).toFixed(precision))
|
||||
}
|
||||
|
|
|
|||
136
src/lib/xpub.ts
136
src/lib/xpub.ts
|
|
@ -1,136 +0,0 @@
|
|||
/**
|
||||
* Extract xpub/tpub from a branch string
|
||||
* Example: "m/84'/1'/0'/0 tpubDCXYZ..." -> "tpubDCXYZ..."
|
||||
* Matches full extended public keys (xpub/ypub/zpub/tpub/vpub) of exactly 111 base58 characters
|
||||
*/
|
||||
export function extractXpubFromBranch(branchStr: string): string | null {
|
||||
// Match full extended public keys (xpub/ypub/zpub/tpub/vpub) of exactly 111 base58 characters
|
||||
const match = branchStr.match(/\b([xtyvz]pub[1-9A-HJ-NP-Za-km-z]{107})\b/)
|
||||
if (!match) return null
|
||||
|
||||
const xpub = match[1]
|
||||
// Defensive check in case the regex is modified in the future
|
||||
return xpub.length === 111 ? xpub : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract derivation path from a branch string
|
||||
* Example: "m/84'/1'/0'/0 tpubDCXYZ..." -> "m/84'/1'/0'/0"
|
||||
*/
|
||||
export function extractDerivationPath(branchStr: string): string | null {
|
||||
const match = branchStr.match(/(m\/[\d'/]+)/)
|
||||
return match ? match[1] : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Base58 alphabet for Bitcoin addresses
|
||||
*/
|
||||
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
||||
|
||||
/**
|
||||
* Decode base58check string to bytes
|
||||
*/
|
||||
function base58Decode(str: string): Uint8Array {
|
||||
const bytes: number[] = []
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
let carry = BASE58_ALPHABET.indexOf(str[i])
|
||||
if (carry < 0) throw new Error('Invalid base58 character')
|
||||
|
||||
for (let j = 0; j < bytes.length; j++) {
|
||||
carry += bytes[j] * 58
|
||||
bytes[j] = carry & 0xff
|
||||
carry >>= 8
|
||||
}
|
||||
|
||||
while (carry > 0) {
|
||||
bytes.push(carry & 0xff)
|
||||
carry >>= 8
|
||||
}
|
||||
}
|
||||
|
||||
// Add leading zeros
|
||||
for (let i = 0; i < str.length && str[i] === '1'; i++) {
|
||||
bytes.push(0)
|
||||
}
|
||||
|
||||
return new Uint8Array(bytes.reverse())
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode bytes to base58check string
|
||||
*/
|
||||
function base58Encode(buffer: Uint8Array): string {
|
||||
const digits = [0]
|
||||
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
let carry = buffer[i]
|
||||
for (let j = 0; j < digits.length; j++) {
|
||||
carry += digits[j] << 8
|
||||
digits[j] = carry % 58
|
||||
carry = (carry / 58) | 0
|
||||
}
|
||||
|
||||
while (carry > 0) {
|
||||
digits.push(carry % 58)
|
||||
carry = (carry / 58) | 0
|
||||
}
|
||||
}
|
||||
|
||||
// Add leading zeros
|
||||
for (let i = 0; i < buffer.length && buffer[i] === 0; i++) {
|
||||
digits.push(0)
|
||||
}
|
||||
|
||||
return digits
|
||||
.reverse()
|
||||
.map((d) => BASE58_ALPHABET[d])
|
||||
.join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert xpub/tpub to native segwit format (zpub/vpub) for BIP84
|
||||
* Uses SLIP-0132 version bytes:
|
||||
* - xpub (0x0488b21e) -> zpub (0x04b24746) for mainnet P2WPKH
|
||||
* - tpub (0x043587cf) -> vpub (0x045f1cf6) for testnet P2WPKH
|
||||
*/
|
||||
export async function toNativeSegwitPub(xpub: string): Promise<string> {
|
||||
try {
|
||||
// Decode the extended public key
|
||||
const decoded = base58Decode(xpub)
|
||||
|
||||
if (decoded.length !== 82) {
|
||||
// Invalid length, return original
|
||||
return xpub
|
||||
}
|
||||
|
||||
// Extract version bytes (first 4 bytes)
|
||||
const version = (decoded[0] << 24) | (decoded[1] << 16) | (decoded[2] << 8) | decoded[3]
|
||||
|
||||
// SLIP-0132 version mapping
|
||||
let newVersion: number
|
||||
if (version === 0x0488b21e) {
|
||||
// xpub -> zpub (mainnet)
|
||||
newVersion = 0x04b24746
|
||||
} else if (version === 0x043587cf) {
|
||||
// tpub -> vpub (testnet)
|
||||
newVersion = 0x045f1cf6
|
||||
} else {
|
||||
// Already in native segwit format or unknown, return original
|
||||
return xpub
|
||||
}
|
||||
|
||||
// Create new buffer with updated version
|
||||
const newDecoded = new Uint8Array(decoded)
|
||||
newDecoded[0] = (newVersion >> 24) & 0xff
|
||||
newDecoded[1] = (newVersion >> 16) & 0xff
|
||||
newDecoded[2] = (newVersion >> 8) & 0xff
|
||||
newDecoded[3] = newVersion & 0xff
|
||||
|
||||
// Re-encode with new version
|
||||
return base58Encode(newDecoded)
|
||||
} catch (error) {
|
||||
console.error('Error converting xpub to native segwit format:', error)
|
||||
// If conversion fails, return the original xpub
|
||||
return xpub
|
||||
}
|
||||
}
|
||||
103
src/lib/xpubs.test.ts
Normal file
103
src/lib/xpubs.test.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { HDKey } from '@scure/bip32'
|
||||
import { mnemonicToSeedSync } from '@scure/bip39'
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { DUMMY_SEED_PHRASE } from './utils'
|
||||
import { convertExtendedPublicKey } from './xpubs'
|
||||
|
||||
// from https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vector-1
|
||||
const BIP32_TEST_VECTOR_1 = {
|
||||
0: 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8',
|
||||
1: 'xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw',
|
||||
}
|
||||
|
||||
describe('xpubs', () => {
|
||||
it('convert xpub to other formats from BIP32 test vectors', () => {
|
||||
const vector0 = BIP32_TEST_VECTOR_1[0]
|
||||
const vector1 = BIP32_TEST_VECTOR_1[1]
|
||||
|
||||
expect(convertExtendedPublicKey(vector0, 'xpub')).toBe(
|
||||
'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8',
|
||||
)
|
||||
expect(convertExtendedPublicKey(vector0, 'ypub')).toBe(
|
||||
'ypub6QqdH2c5z7967BioGSfAWFHM1EHzHPBZK7wrND3ZpEWFtzmCqvsD1bgpaE6pSAPkiSKhkuWPCJV6mZTSNMd2tK8xYTcJ48585pZecmSUzWp',
|
||||
)
|
||||
expect(convertExtendedPublicKey(vector0, 'zpub')).toBe(
|
||||
'zpub6jftahH18ngZxUuv6oSniLNrBCSSE1B4EEU59bwTCEt8x6aS6b2mdfLxbS4QS53g85SWWP6wexqeer516433gYpZQoJie2tcMYdJ1SYYYAL',
|
||||
)
|
||||
expect(convertExtendedPublicKey(vector0, 'tpub')).toBe(
|
||||
'tpubD6NzVbkrYhZ4XgiXtGrdW5XDAPFCL9h7we1vwNCpn8tGbBcgfVYjXyhWo4E1xkh56hjod1RhGjxbaTLV3X4FyWuejifB9jusQ46QzG87VKp',
|
||||
)
|
||||
expect(convertExtendedPublicKey(vector0, 'upub')).toBe(
|
||||
'upub57Wa4MvRPNyAhzxKw1WfftuLKMiCWuDZefryEdU2JCzjgbWHqJCxXM4GVQGUSXn55srUm189Mf4uER1BVZxyhNQZ56pbiUoAzvK54VEYrWu',
|
||||
)
|
||||
expect(convertExtendedPublicKey(vector0, 'vpub')).toBe(
|
||||
'vpub5SLqN2bLY4WeZJ9SmNJHsyzqVKreTXD4ZnPC22MugDNcjhKX5xNX9QiQWcE4SSRzVWyHWUihpKRT7hckDGNzVc69wSX2JPcfGeNiT5c2XZy',
|
||||
)
|
||||
|
||||
expect(convertExtendedPublicKey(vector1, 'xpub')).toBe(
|
||||
'xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw',
|
||||
)
|
||||
expect(convertExtendedPublicKey(vector1, 'ypub')).toBe(
|
||||
'ypub6T73GjuZ5NG5FnrWUCXoPHPTL3rLfTfZzjNkLJRgnRhYGH4PGAQJ8k3EMVfXBUJHiecGd93ovwZBjxRaKPMQxCbgk6QYyRyLbkhCvXJ8PtA',
|
||||
)
|
||||
expect(convertExtendedPublicKey(vector1, 'zpub')).toBe(
|
||||
'zpub6mwJaQaUE3oZ763dJZKRbNUxW1znc5f4uqty7hKaAS5RKNscWpZrkohNNhd7BNxD8Hj5NceNPbujdF3935mRkSHHcS6yZLnpsUkrK1XoMLr',
|
||||
)
|
||||
expect(convertExtendedPublicKey(vector1, 'tpub')).toBe(
|
||||
'tpubD8eQVK4Kdxg3gHrF62jGP7dKVCoYiEB8dFSpuTawkL5YxTus5j5pf83vaKnii4bc6v2NVEy81P2gYrJczYne3QNNwMTS53p5uzDyHvnw2jm',
|
||||
)
|
||||
expect(convertExtendedPublicKey(vector1, 'upub')).toBe(
|
||||
'upub59mz45DtUe69rc638mPJYw1SeBGYtyhaLHHsCir9GQC23soUFXk3eVQgGfqBBqgc6693dEfa6J8zCoyKSbhMmFsHGjcrdnhPWrSdN8uUxKb',
|
||||
)
|
||||
expect(convertExtendedPublicKey(vector1, 'vpub')).toBe(
|
||||
'vpub5UcFMjtodKddhuH9y8Avm26wp9Qzqbh5FPp5z7k2eQZu6ychWBucGZ4pHsnmBkLXVjFrNiG8YxVY66atAJ7NZVYt95KHDhWsnaWGkhF4DrT',
|
||||
)
|
||||
})
|
||||
|
||||
it('convert xpub to other formats and back from BIP32 test vectors', () => {
|
||||
const vector0 = BIP32_TEST_VECTOR_1[0]
|
||||
|
||||
expect(
|
||||
[vector0]
|
||||
.map((it) => convertExtendedPublicKey(it, 'xpub'))
|
||||
.map((it) => convertExtendedPublicKey(it, 'ypub'))
|
||||
.map((it) => convertExtendedPublicKey(it, 'Ypub'))
|
||||
.map((it) => convertExtendedPublicKey(it, 'zpub'))
|
||||
.map((it) => convertExtendedPublicKey(it, 'Zpub'))
|
||||
.map((it) => convertExtendedPublicKey(it, 'tpub'))
|
||||
.map((it) => convertExtendedPublicKey(it, 'upub'))
|
||||
.map((it) => convertExtendedPublicKey(it, 'Upub'))
|
||||
.map((it) => convertExtendedPublicKey(it, 'vpub'))
|
||||
.map((it) => convertExtendedPublicKey(it, 'Vpub'))
|
||||
.map((it) => convertExtendedPublicKey(it, 'xpub'))[0],
|
||||
).toBe(vector0)
|
||||
})
|
||||
|
||||
it('convert xpub to zpub from dummy seed phrase', () => {
|
||||
const seed = mnemonicToSeedSync(DUMMY_SEED_PHRASE.join(' '))
|
||||
const root = HDKey.fromMasterSeed(seed)
|
||||
const xpub32_0 = root.derive(`m/44'/0'/0'`).publicExtendedKey
|
||||
const xpub84_0 = root.derive(`m/84'/0'/0'`).publicExtendedKey
|
||||
const xpub84_4 = root.derive(`m/84'/0'/4'`).publicExtendedKey
|
||||
|
||||
expect(xpub32_0, 'sanity check').toBe(
|
||||
'xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj',
|
||||
)
|
||||
expect(xpub84_0, 'sanity check').toBe(
|
||||
'xpub6CatWdiZiodmUeTDp8LT5or8nmbKNcuyvz7WyksVFkKB4RHwCD3XyuvPEbvqAQY3rAPshWcMLoP2fMFMKHPJ4ZeZXYVUhLv1VMrjPC7PW6V',
|
||||
)
|
||||
expect(xpub84_4, 'sanity check').toBe(
|
||||
'xpub6CatWdiZiodmeXswr13Gd5aNtNqr2UHCBEsCoL3eEFVaM7n8kY5kS4daaP83gWQncmzL3Wzt79mEiLix6XZs6XQmGcQNeQ4HcjfVTn9TuXE',
|
||||
)
|
||||
|
||||
expect(convertExtendedPublicKey(xpub32_0, 'zpub')).toBe(
|
||||
'zpub6qUQGY8YyN3ZxYEgf8J6KCQBqQAbdSWaT9RK54L5FWTTh8na8NkCkZpYHnWt7zEwNhqd6p9Utq562cSZsqGqFE87NNsUKnyZeJ5KvbhfC8E',
|
||||
)
|
||||
expect(convertExtendedPublicKey(xpub84_0, 'zpub')).toBe(
|
||||
'zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs',
|
||||
)
|
||||
expect(convertExtendedPublicKey(xpub84_4, 'zpub')).toBe(
|
||||
'zpub6rFR7y4Q2AijM8GBWicX3FmPEK8juiGC1TueN7qQzGFLTKQbFrQsgBwrco3DgKidS4DwYUC12UULUux5XvPtgzmy1HoDpDhGABnnEyBQzsL',
|
||||
)
|
||||
})
|
||||
})
|
||||
54
src/lib/xpubs.ts
Normal file
54
src/lib/xpubs.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { sha256 } from '@noble/hashes/sha2.js'
|
||||
import { createBase58check } from '@scure/base'
|
||||
|
||||
const base58check = createBase58check(sha256)
|
||||
|
||||
const uint8ArrayfromHex = (hex: string) => {
|
||||
const match = hex.match(/.{1,2}/g)
|
||||
if (match === null) {
|
||||
throw new Error('Cannot convert hex to Uint8Array: Invalid hex string.')
|
||||
}
|
||||
return Uint8Array.from(hex.match(/.{1,2}/g)?.map((byte) => parseInt(byte, 16)) || [])
|
||||
}
|
||||
|
||||
// version bytes for extended serialization of public and private keys.
|
||||
// taken from https://github.com/satoshilabs/slips/blob/master/slip-0132.md
|
||||
const XPUB_VERSION_BYTES = {
|
||||
xpub: uint8ArrayfromHex('0488b21e'),
|
||||
ypub: uint8ArrayfromHex('049d7cb2'),
|
||||
Ypub: uint8ArrayfromHex('0295b43f'),
|
||||
zpub: uint8ArrayfromHex('04b24746'),
|
||||
Zpub: uint8ArrayfromHex('02aa7ed3'),
|
||||
tpub: uint8ArrayfromHex('043587cf'),
|
||||
upub: uint8ArrayfromHex('044a5262'),
|
||||
Upub: uint8ArrayfromHex('024289ef'),
|
||||
vpub: uint8ArrayfromHex('045f1cf6'),
|
||||
Vpub: uint8ArrayfromHex('02575483'),
|
||||
}
|
||||
|
||||
export type XpubFormat = keyof typeof XPUB_VERSION_BYTES
|
||||
|
||||
/*
|
||||
* This function takes an extended public key (with any version bytes, it doesn't need to be an xpub)
|
||||
* and converts it to an extended public key formatted with the desired version bytes
|
||||
* @param xpub: an extended public key in base58 format. Example: xpub6CpihtY9HVc1jNJWCiXnRbpXm5BgVNKqZMsM4XqpDcQigJr6AHNwaForLZ3kkisDcRoaXSUms6DJNhxFtQGeZfWAQWCZQe1esNetx5Wqe4M
|
||||
* @param targetFormat: a string representing the desired format; must exist in the XPUB_VERSION_BYTES mapping defined above. Example: Zpub
|
||||
*/
|
||||
export function convertExtendedPublicKey(xpub: string, targetFormat: XpubFormat) {
|
||||
const versionBytes = XPUB_VERSION_BYTES[targetFormat]
|
||||
if (!versionBytes) {
|
||||
throw new Error('Invalid target format: Unknown version bytes.')
|
||||
}
|
||||
|
||||
try {
|
||||
const decodedXpub = base58check.decode(xpub.trim())
|
||||
const decodedXpubWithoutVersion = decodedXpub.slice(versionBytes.length)
|
||||
const merged = new Uint8Array(versionBytes.length + decodedXpubWithoutVersion.length)
|
||||
merged.set(versionBytes)
|
||||
merged.set(decodedXpubWithoutVersion, versionBytes.length)
|
||||
return base58check.encode(merged)
|
||||
} catch (error: unknown) {
|
||||
const reason = error instanceof Error ? error.message : undefined
|
||||
throw new Error(`Invalid extended public key: ${reason ?? 'Unknown reason.'}`)
|
||||
}
|
||||
}
|
||||
2
src/types/global.d.ts
vendored
2
src/types/global.d.ts
vendored
|
|
@ -3,6 +3,8 @@ export type AmountSats = number
|
|||
export type BitcoinAddress = string
|
||||
export type JarIndex = number
|
||||
|
||||
export type SeedPhrase = string[]
|
||||
|
||||
export type Milliseconds = number
|
||||
export type Seconds = number
|
||||
export type Days = number
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue