@emurgo/links
v0.1.4
Published
Cardano CIP-13 URI + brand deep-link parsing/building for wallet integrations (yoroi://, secondfi://, web+secondfi://, https fallback). Supports payment, browse, transfer, exchange, browser-launch, and chain-aware partner payment links.
Readme
@emurgo/links
Cardano URI parsing and brand-specific deep link handling.
Installation
@emurgo/links is published on GitHub Packages, not the public npm
registry. GitHub Packages requires authentication for npm installs, even for
public packages, so consumers need a GitHub personal access token (PAT) with
the read:packages scope. The token can belong to the consuming developer's
own GitHub account; they do not need to be a member of the Emurgo organization.
Create a GitHub PAT
In GitHub, go to Settings → Developer settings → Personal access tokens → Tokens (classic), create a token with the
read:packagesscope, and expose it locally asGITHUB_TOKEN.export GITHUB_TOKEN=ghp_your_token_hereAdd to your project's
.npmrc(or~/.npmrc):@emurgo:registry=https://npm.pkg.github.com //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}Verify authentication:
npm whoami --registry=https://npm.pkg.github.comInstall:
npm install @emurgo/links effect # or bun add @emurgo/links effect
The effect package is a runtime dependency — parse() returns an
Effect<...> and InvalidUriError is built on Effect's tagged-error helpers.
If your project does not yet use Effect, install it; it's a single small
runtime addition (≈ 30 KB gzipped).
Features
- Parse and build Cardano URIs (
web+cardano://...) per CIP-13 - Legacy CIP-13 bare address format (
web+cardano:<address>?amount=...) - Brand-specific deep links (
yoroi://...,secondfi://...,https://yoroi-wallet.com/w1/...) - 12 URI authorities for the Cardano protocol
- Type-safe with Effect-TS
- Round-trip parsing (parse -> build -> same URI)
Cardano URIs (web+cardano://)
Supported Authorities
| Authority | Format | Purpose |
|-----------|--------|---------|
| claim | web+cardano://claim/<faucet>/<code> | Token faucet claims (CIP-99) |
| browse | web+cardano://browse/<type>/<id> | Block explorer links (CIP-158) |
| pay | web+cardano://pay?address=...&amount=... | Payment requests (CIP-13) |
| payment | Alias for pay | Legacy compatibility |
| stake | web+cardano://stake/<pool_id> | Stake pool delegation |
| drep | web+cardano://drep/<drep_id> | DRep delegation |
| transaction | web+cardano://transaction/<hash> | View transaction (CIP-107) |
| block | web+cardano://block/<hash> | View block (CIP-107) |
| address | web+cardano://address/<addr> | View address (CIP-134) |
| connect | web+cardano://connect?... | Wallet connect |
| wallet | web+cardano://wallet?encrypted=... | Wallet import |
| legacy-transfer | web+cardano://legacy-transfer?... | Byron wallet transfer |
The legacy CIP-13 bare address format web+cardano:<address>?amount=...&memo=... is also supported and parses as a pay authority.
Parsing
import { parse, isCardanoUri } from '@emurgo/links'
import { Effect } from 'effect'
// Parse a payment URI
const result = await Effect.runPromise(
parse('web+cardano://pay?address=addr1...&amount=1000000')
)
// { authority: 'pay', address: 'addr1...', amount: 1000000n }
// Legacy CIP-13 bare address format also works
const legacy = await Effect.runPromise(
parse('web+cardano:addr1...?amount=5000000')
)
// { authority: 'pay', address: 'addr1...', amount: 5000000n }
// Type guard
isCardanoUri('web+cardano://pay?address=addr1...') // true
isCardanoUri('web+cardano:addr1...') // true (legacy)
isCardanoUri('https://example.com') // falseBuilding
import { build } from '@emurgo/links'
const payUri = build({
authority: 'pay',
address: 'addr1...',
amount: 5000000n,
message: 'Coffee'
})
// 'web+cardano://pay?address=addr1...&amount=5000000&message=Coffee'
const stakeUri = build({ authority: 'stake', poolId: 'pool1abc' })
// 'web+cardano://stake/pool1abc'Direct Authority Access
import { Authorities } from '@emurgo/links'
const uri = Authorities.Pay.build('addr1...', 1000000n, 'Tip')Brand-Specific Links
Brand links handle brand-specific deep links that use the native brand scheme (yoroi://, secondfi://), the browser-extension-safe web scheme (web+secondfi://), or HTTPS fallback (https://yoroi-wallet.com/w1/...).
Supported Use Cases
| Feature | Use Case | Purpose |
|---------|----------|---------|
| transfer | request/ada | Multi-target ADA payment request |
| transfer | request/ada-with-link | Payment with embedded web+cardano:// link |
| transfer | request/crypto | Chain-aware native/token payment request |
| exchange | order/show-create-result | Exchange order result display |
| browser | launch | Launch dApp URL in in-app browser |
All brand links support partner integration fields (appId, authorization, signature, redirectTo).
Parsing Brand Links
import { parseBrandLink, isBrandLink } from '@emurgo/links'
// Detect brand links
isBrandLink('yoroi://yoroi-wallet.com/w1/browser/launch?dappUrl=...', 'yoroi') // true
isBrandLink('https://yoroi-wallet.com/w1/browser/launch?dappUrl=...') // true
isBrandLink('web+secondfi://secondfi.io/w1/browser/launch?dappUrl=...', 'secondfi') // true
// Parse
const action = parseBrandLink(
'yoroi://yoroi-wallet.com/w1/browser/launch?dappUrl=https%3A%2F%2Fapp.com',
'yoroi'
)
// { feature: 'browser', useCase: 'launch', params: { dappUrl: 'https://app.com' } }Building Brand Links
import { buildBrandLink } from '@emurgo/links'
const url = buildBrandLink(
{ feature: 'browser', useCase: 'launch', params: { dappUrl: 'https://app.com' } },
'yoroi'
)
// 'yoroi://yoroi-wallet.com/w1/browser/launch?dappUrl=https%3A%2F%2Fapp.com'Chain-Aware Transfer Requests
Partner integrations can request a concrete wallet send with transfer/request/crypto.
The amount value is base units: lovelace for ADA, wei for ETH, and token
base units for ERC-20s such as USDC.
Pass redirectTo as the raw HTTPS return URL; the builder applies the query
encoding and preserves nested callback parameters.
This is the recommended SecondFi/Encryptus callback format for off-ramp deposit
requests. It replaces the old Yoroi/Cardano-only flow that first built a
web+cardano://pay?... link and then wrapped it with transfer.request.adaWithLink.
Partners should call buildTransferRequestCryptoLink directly for both ADA and
supported EVM transfers.
Accepted chains are exported as a const tuple so partners get IDE autocomplete:
import { KNOWN_TRANSFER_CHAINS, type KnownTransferChain } from '@emurgo/links'
KNOWN_TRANSFER_CHAINS // readonly ['cardano:mainnet', 'eip155:1']import { buildTransferRequestCryptoLink } from '@emurgo/links'
const url = buildTransferRequestCryptoLink({
chain: 'eip155:1',
asset: 'ETH',
address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
amount: '50000000000000000',
redirectTo: 'https://hub.encryptus.co/pw/?orderId=123',
appId: '18d1545a-a59b-45cb-a180-157b110c77fe',
})
// 'secondfi://secondfi.io/w1/transfer/request/crypto?...'For browser-extension callbacks, use the web+secondfi scheme option because
extensions register web protocol handlers such as web+secondfi, not the
native-app secondfi:// scheme:
const extensionUrl = buildTransferRequestCryptoLink(
{
chain: 'ETHEREUM',
name: 'USDC',
address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
amount: '5050000',
client: 'encryptus',
callbackPlatform: 'web',
},
{ scheme: 'web+secondfi' }
)
// 'web+secondfi://secondfi.io/w1/transfer/request/crypto?...'Encryptus Integration Snippet
import { buildTransferRequestCryptoLink } from '@emurgo/links'
const createSecondfiLink = ({
chain,
name,
address,
amount,
callbackPlatform,
sourceWalletGroupId,
sourceWalletAccountIndex,
}: {
chain: 'CARDANO' | 'ETHEREUM' | 'ETH'
name: 'ADA' | 'USDC' | 'ETH'
address: string
amount: string // base-unit integer string, e.g. 5050000 for 5.05 USDC
callbackPlatform: 'web' | 'app'
sourceWalletGroupId?: string // echo the value SecondFi passed to the Encryptus widget
sourceWalletAccountIndex?: string // echo with sourceWalletGroupId when provided
}) =>
buildTransferRequestCryptoLink(
{
client: 'encryptus',
chain,
name,
address,
amount,
callbackPlatform,
sourceWalletGroupId,
sourceWalletAccountIndex,
redirectTo: window.location.href,
appId: '18d1545a-a59b-45cb-a180-157b110c77fe',
authorization: 'uuid-v4',
},
{ scheme: callbackPlatform === 'web' ? 'web+secondfi' : 'secondfi' },
)Important integration notes:
client='encryptus'enables the Encryptus label normalizer.callbackPlatform='web'tells Encryptus this callback is for the browser extension and should useweb+secondfi://...; usecallbackPlatform='app'for native mobile app callbacks andsecondfi://....- If SecondFi passes
sourceWalletGroupIdandsourceWalletAccountIndexto the Encryptus widget, echo them back unchanged inbuildTransferRequestCryptoLink. The wallet uses them to restore the source wallet and account selected when the off-ramp handoff started. - Encryptus
chain='CARDANO'withname='ADA'maps tochain='cardano:mainnet'andasset='ADA'. - Encryptus
chain='ETHEREUM'withname='USDC'maps tochain='eip155:1',asset='USDC', and the Ethereum mainnet USDC contract token. - Encryptus
chain='ETH'/chain='ETHEREUM'withname='ETH'maps to native Ethereum. Theamountmust already be wei. - Without a supported
client, pass canonical values directly, for examplechain='cardano:mainnet'orchain='eip155:1'. amountmust be an integer string in base units. For example,2000000means2 ADA, and5050000means5.05 USDC.- Pass
redirectToas a raw URL. Do not wrap it withencodeURIComponent; this helper handles query encoding. - If
window.location.hrefcontains very large transient state, prefer a shorter HTTPS order/status URL that can resume the Encryptus session. - The generated link uses
web+secondfi://secondfi.io/w1/...forcallbackPlatform='web'andsecondfi://secondfi.io/w1/...forcallbackPlatform='app'.
Error Handling
import { parse, InvalidUriError } from '@emurgo/links'
import { Effect, Exit } from 'effect'
const result = await Effect.runPromiseExit(parse('invalid-uri'))
if (Exit.isFailure(result)) {
// Handle InvalidUriError
console.error('Parse failed:', result.cause)
}Development
The package is part of the chimera monorepo. To run its tests from a checkout:
bun vitest run packages/links