Platform-agnostic core for Tapylet — the Tapyrus wallet logic shared across clients (browser extension, and future web/mobile front-ends).
This package contains zero UI / browser / Chrome-extension dependencies. It exposes the HD wallet, transaction building, token issuance, Esplora API client, and storage interfaces. Persistence is left to the consumer via a small adapter interface, so the same logic runs in any JavaScript environment.
npm install @tapylet/core
# or
pnpm add @tapylet/coreThese two points are mandatory for the package to work in a bundled (browser) build. Skipping them produces confusing runtime/build errors.
tapyrusjs-lib transitively depends on the real
tiny-secp256k1, which uses
WASM + Node's fs and cannot run in a browser. This package ships a
@noble-based drop-in replacement
at @tapylet/core/lib/secp256k1-compat. Alias tiny-secp256k1 to it in your
bundler config.
For a Parcel/Plasmo consumer, in the consumer's package.json:
For other bundlers use the equivalent alias mechanism (e.g. Vite/Rollup
resolve.alias, webpackresolve.alias) pointing at the same file.
This package publishes its public API through the exports map (subpath
exports). Legacy "moduleResolution": "node" (a.k.a. node10) does not read
exports and will fail with TS2307 on subpath imports. Use:
{
"compilerOptions": {
"moduleResolution": "bundler" // or "node16" / "nodenext"
}
}The whole API is available from the root, or via subpath exports that mirror the source layout.
// Root barrel — everything in one import
import { generateMnemonic, createHDWallet, getBalance } from "@tapylet/core"
// Or subpath exports
import { generateMnemonic, createHDWallet } from "@tapylet/core/wallet"
import { getBalance, broadcastTransaction } from "@tapylet/core/api"
import { WalletStorage } from "@tapylet/core/storage/walletStorage"import {
generateMnemonic,
validateMnemonic,
createHDWallet,
generateAddress,
createAndSignTransaction,
} from "@tapylet/core/wallet"
const mnemonic = generateMnemonic() // 12-word BIP39 phrase (strength 128)
const keys = await createHDWallet(mnemonic) // { privateKey, publicKey, wif }
const address = generateAddress(keys.publicKey)
// Build + sign a TPC transfer (does not broadcast)
const { txid, txHex } = await createAndSignTransaction({
fromAddress: address,
toAddress: "...",
amount: 1000, // tapyrus
mnemonic,
})split (1-100) pays the recipient through that many outputs instead of one, so
the recipient can spend in parallel instead of chaining unconfirmed
transactions. Every output gets floor(amount / split) and the whole remainder
goes to the last output, so that output can exceed the others by up to
split - 1: an amount of 199 split 100 ways yields 99 outputs of 1 and one
output of 100. For TPC every output must clear the dust threshold.
createAndSignAssetTransaction accepts the same option for Colored Coins, where
an amount smaller than split yields amount outputs of 1 instead.
createAndSignTransaction requires an uncolored recipient address, because it
spends TPC inputs only and so cannot fund a colored output.
feeRate is given in tapyrus per byte and must be between 1 and 1000. Nodes
relay a transaction only at 1 or more, and a rate above 1000 costs more in fees
than any transfer is worth, so both ends are rejected before the transaction is
built.
estimateFee returns the fee a transfer of the same arguments would pay,
including a leftover too small to become its own change output:
const fee = await estimateFee(address, 1000, { feeRate: 10, split: 4 })import { issueToken } from "@tapylet/core"
const result = await issueToken({
tokenType: "reissuable", // "reissuable" | "non_reissuable" | "nft"
amount: 100,
metadata: { /* MetadataFields */ },
mnemonic,
fromAddress: address,
})
// { txid, colorId, paymentBase, outPoint? }import {
getBalance,
getAllBalances,
broadcastTransaction,
getTokenMetadata,
} from "@tapylet/core/api"
const balance = await getBalance(address)
const txid = await broadcastTransaction(txHex)The core defines interfaces, not implementations. The consumer provides a
KeyValueStore and a SecureKeyValueStore backed by its platform
(chrome.storage, localStorage, SQLite, …) and wires them into the store
classes.
import {
WalletStorage,
IssuedTokenStore,
type KeyValueStore,
type SecureKeyValueStore,
} from "@tapylet/core"
// Implement these for your platform:
class MyKeyValueStore implements KeyValueStore { /* get/set/remove/watch */ }
class MySecureStore implements SecureKeyValueStore { /* setPassword/get/set/remove */ }
const walletStorage = new WalletStorage(new MySecureStore(), new MyKeyValueStore())
await walletStorage.setPassword("…")
await walletStorage.unlock("…")
const wallet = await walletStorage.getWallet()| Subpath | Exports |
|---|---|
@tapylet/core/wallet |
generateMnemonic, validateMnemonic, mnemonicToSeed, createHDWallet, getKeyPairFromMnemonic, generateAddress, validateAddress, createAndSignTransaction, estimateFee, createAndSignAssetTransaction, burnAsset, … |
@tapylet/core (issuance) |
issueToken, TokenType, MetadataFields, IssueOptions, IssueResult |
@tapylet/core/api |
getBalance, getAllBalances, getAddressUtxos, broadcastTransaction, getTransactionInfo, getTokenMetadata, formatTpc, formatColorId, TPC_COLOR_ID, … |
@tapylet/core/storage/* |
WalletStorage, IssuedTokenStore, PendingTxStore, SettingsStore, KeyValueStore, SecureKeyValueStore |
@tapylet/core (types/constants) |
WalletData, WalletState, DEFAULT_AUTO_LOCK_MINUTES, AUTO_LOCK_OPTIONS, … |
@tapylet/core/utils/sanitize |
sanitizeUrl, sanitizeImageUrl |
All subpaths are also reachable from the root barrel @tapylet/core.
Token metadata is written by the issuer, and any issuer can send a token to any address, so URLs read out of metadata are untrusted. Pass them through the sanitizers before rendering.
sanitizeUrlacceptshttps:,http:andipfs:, and prefixeshttps://when the input carries no scheme. A host followed by a port (example.com:8080/a,intranet:8443) counts as carrying no scheme.https:andhttp:must name a host:https:example.comhas no authority and a browser resolves it against the page it is rendered on, so it returnsundefined. Everything else,javascript:anddata:included, returnsundefined.sanitizeImageUrlacceptshttps:URLs anddata:URLs declaring a raster image media type (png,jpeg,jpg,gif,webp), and prefixeshttps://when the input carries no scheme.http:,ipfs:anddata:image/svg+xmlreturnundefined.
What sanitizeImageUrl does not promise. The check stops at the URL, so
what comes back is only safe to render through <img>, which keeps an SVG
inert whatever the bytes turn out to be.
- For a
data:URL the media type is what the URL declares about itself. The payload is never decoded, sodata:image/png;base64,<an SVG>passes. - For an
https:URL nothing about the response is known at all, sohttps://example.com/icon.svgpasses.
A consumer that fetches the image and inlines it, or renders it through
<object>, <embed> or a WebView, needs its own guard.
pnpm install
pnpm run build # tsc -> dist/ (CommonJS + .d.ts)
pnpm test # jestTo iterate on the core while developing a consumer, link it instead of pinning the published version:
# in the consumer
pnpm link ../tapylet-coreRemember the core ships compiled dist/, so run pnpm run build after each
change to reflect it in the linked consumer.
npm version patch # bump version
npm publish # access=public is set via publishConfig; 2FA/OTP requiredMIT
{ "alias": { "fs": false, "path": false, // Must be a ROOT-RELATIVE FILE PATH, not a package specifier. // Parcel resolves file-path alias targets relative to the package.json // that defines the alias, so this works regardless of where (deep in the // dependency tree) the import originates. "tiny-secp256k1": "./node_modules/@tapylet/core/dist/lib/secp256k1-compat.js" } }