@solidus-network/wallet
v0.1.0
Published
Solidus wallet SDK — did:solidus keypair derivation, an injectable verifiable-credential store, BBS+ selective-disclosure presentation, and scoped payment-mandate stamping. Publishes as @solidus-network/wallet.
Maintainers
Readme
@solidus-network/wallet
A wallet SDK for the Solidus Network: derive a did:solidus identity from a BIP-39 mnemonic, hold
verifiable credentials in a store you supply, present them with BBS+ selective disclosure, and stamp
scoped payment mandates for an agent to spend against.
Private keys live in a closure and are never returned.
Status — read this first
- First release (0.1.0), testnet-grade. The API is small on purpose; treat it as a foundation, not a finished wallet.
- Nothing is bundled that you might want to swap. The credential store and the BBS+ implementation are both injected. The only built-in store is in-memory.
- There is no key-export function. This is deliberate (see below) and it is also a limitation: a mnemonic you did not save at creation time is unrecoverable through this API.
Install
npm install @solidus-network/walletQuick start
import { createWallet, newMnemonic } from '@solidus-network/wallet'
// Generate and SAVE this — the wallet will not give it back to you.
const mnemonic = newMnemonic()
const wallet = await createWallet({ mnemonic, network: 'testnet' })
console.log(wallet.did) // did:solidus:testnet:7Kf9...
console.log(wallet.address) // 7Kf9...
await wallet.addCredential(someVerifiableCredential)
const all = await wallet.listCredentials()Keys
import { newMnemonic, isValidMnemonic, deriveWalletKeys } from '@solidus-network/wallet'| Function | Does |
|---|---|
| newMnemonic() | Fresh 12-word BIP-39 mnemonic (128-bit entropy) |
| isValidMnemonic(phrase) | Checksum + wordlist validation |
| deriveWalletKeys(mnemonic, network?) | The full derivation, returned as plain data |
| addressFromPublicKeyBytes(bytes) | base58(BLAKE3(publicKey)[0..20]) |
Derivation is deterministic — the same mnemonic always produces the same DID:
phrase → BIP-39 PBKDF2 seed (64 bytes) → first 32 bytes = Ed25519 private key
address = base58(BLAKE3(publicKey)[0..20])
did = did:solidus:{network}:{address}
deriveWalletKeysreturnsmnemonicandprivKeyHexin plain text. That is the point of the function — it is the low-level escape hatch — but it means the result is key material. Do not log it, return it over a wire, or put it in an error message. If you only need an identity and a credential store, usecreateWallet, which keeps both values in a closure and never exposes them.
Credential storage
interface CredentialStore {
add(vc: VerifiableCredential): Promise<void>
list(): Promise<VerifiableCredential[]>
get(id: string): Promise<VerifiableCredential | undefined>
}memoryStore() is the default and is not persistent — everything is gone when the process
exits. Implement the same three methods over IndexedDB, SQLite, a Solidus pod, or anything else and
pass it in:
const wallet = await createWallet({ mnemonic, store: myIndexedDbStore })Selective disclosure
wallet.present(vcId, disclose) reveals only the named fields of a held credential and proves the
rest without showing them. No BBS+ implementation is bundled, and none of the published packages
drops in as one — you supply an object satisfying BbsLike:
interface BbsLike {
present(
vc: VerifiableCredential,
disclose: string[],
): Promise<{ disclosed: string[]; proof: string }>
}const wallet = await createWallet({ mnemonic, bbs: myBbsAdapter })
const { disclosed, proof } = await wallet.present(vcId, ['name', 'age_over_18'])You will have to write the adapter.
@solidus-network/bbsprovides the cryptographic primitives (BbsSecretKey,BbsPublicKey,BbsSignature,BbsProof) and is byte-compatible with the chain — but it works in terms of message indices (disclosedIndices: number[]), not credential field names. Bridging "disclosenameandage_over_18" to the indices those claims occupy in the signed message vector is the adapter's job, and that mapping depends on how your issuer canonicalizes the credential. Passing thebbspackage itself asbbswill not type-check and will not work.
Calling present() without a bbs implementation throws with an explicit message rather than
failing silently. Nothing else in the package requires it.
Payment mandates
stampMandate signs a scoped authorization the holder hands to an agent: pay this merchant, up to
this amount, before this expiry. The agent can act within that envelope and cannot exceed it —
the limit is inside the signed payload, so raising it requires the holder's key.
import { createWallet, stampMandate } from '@solidus-network/wallet'
import type { WalletInternal } from '@solidus-network/wallet'
const wallet = await createWallet({ mnemonic })
const { id, token } = await stampMandate(wallet as WalletInternal, {
merchant: 'baku-bookshop',
maxAmount: 500,
rail: 'moka', // 'moka' | 'x402'
ttlMinutes: 15, // default 15, hard ceiling 120
claims: { age_over_18: true },
})Returns { id, token } and nothing else — no key material. token is a compact JWS:
base64url(header).base64url(payload).base64url(ed25519 signature)
payload: { iss, aud, jti, merchant, max_amount_try, rail, exp, claims }
header: { alg: 'EdDSA', typ: 'JWT', kid: <issuer DID> }Any verifier that checks an EdDSA signature over header.body against the issuer's public key
accepts it. Enforce scope yourself: check exp, merchant, and max_amount_try against the
transaction before honouring it — the signature proves authorization, not appropriateness.
max_amount_tryis TRY-denominated in its name, kept for wire-shape parity with the reference implementation this format mirrors. On thex402(crypto) rail that name stretches its meaning. Known tension, not resolved in 0.1.0 — do not read the suffix as a currency guarantee.
Why the key never comes back
createWallet holds the mnemonic and private key in its closure and returns an object with no
accessor for either. Signing happens through an internal capability that takes bytes and returns a
signature, so stampMandate can sign without the key ever crossing a function boundary it could be
captured at.
The tradeoff is real and one-directional: save the mnemonic when you generate it. There is no recovery path through this API.
What this package does not do
- No network calls. It does not resolve DIDs, submit transactions, or fetch credentials. Pair it
with
@solidus-network/sdkfor chain operations. - No credential issuance or verification. It stores and presents what you put in it.
- No persistence, no encryption at rest. Both are the injected store's job.
License
Apache-2.0
