@asevault/connect
Connect an Asentum dApp to the AseVault mobile wallet (ML-DSA-65 signing). Keys stay on the device; every request needs in-app approval. Zero dependencies.
npm install @asevault/connect
Usage
import { AseVaultProvider } from '@asevault/connect';
const wallet = new AseVaultProvider({ name: 'My dApp' });
await wallet.connect(); // approve in AseVault -> session
const address = wallet.address; // ase1...
const { txHash } = await wallet.callContract({
to: 'ase1...', // or 0x...
method: 'swapExactIn',
args: ['1', '0xeeee...', '1000000000000000000', '990000000000000000'],
});
const { txHash: deployTx, address: contract } = await wallet.deploy(source);
const { txHash: sendTx } = await wallet.sendTransaction({ to: 'ase1...', amount: '1000000000000000000' });
const { signature } = await wallet.signMessage({ message: 'hello' });
const { status, receipt } = await wallet.waitForTx(txHash);
// status: 'confirmed' | 'reverted' | 'timeout'
// timeout: no receipt landed. Reverted txs are dropped without a receipt
// on Asentum, so a timeout usually means dropped or reverted.
const gas = await wallet.estimateGas({ to: 'ase1...', method: 'swapExactIn', args });
wallet.on('chainChanged', (id) => {});
wallet.disconnect();
EIP-1193 style request() is supported with the same method names as the injected provider:
await wallet.request({ method: 'ase_sendCall', params: { to, method: 'mint', args: [] } });
await wallet.request({ method: 'eth_blockNumber' });
await wallet.request({ method: 'estimateGas', params: { to, method: 'mint', args: [] } });
await wallet.accounts();
await wallet.getAddress();
wallet.selectedAddress;
wallet.chainId;
Transports
Picked automatically, or forced via new AseVaultProvider({ transport }):
| Where the dApp runs | Transport | UX |
|---|---|---|
| AseVault in-app browser | InAppTransport | In-place approval sheets |
| Mobile browser | DeepLinkTransport | App switch, approve, back to tab |
| Desktop browser | RelayTransport | QR on screen, phone approves |
On desktop the provider defaults to the public relay (relay.asepad.fun) and connect() shows the built-in QR modal. Pass relay: false to opt out (connect then fails with WALLET_NOT_INSTALLED).
const wallet = new AseVaultProvider({ relay: true });
await wallet.connect(); // QR modal opens automatically
// Per-call override:
await wallet.connect({ ui: 'none' });
// Render the QR yourself:
const wallet2 = new AseVaultProvider({ relay: true, ui: 'none' });
wallet2.on('uri', (uri) => renderQr(uri));
// Custom renderer:
const wallet3 = new AseVaultProvider({ relay: true, ui: (uri) => myQr(uri) });
// Custom relay URL:
const wallet4 = new AseVaultProvider({ relay: 'https://your-relay' });
// Explicit transport:
import { RelayTransport } from '@asevault/connect';
const wallet5 = new AseVaultProvider({
transport: new RelayTransport('https://your-relay', { onUri: (uri) => renderQr(uri) }),
});
Relay latency and failures
The wallet polls its request queue every ~2.5 s while foregrounded, and the result poll is 1 s. Expect a few seconds between callContract() and the approval sheet on the phone. If the wallet is backgrounded, the OS decides when it wakes. Show a "check your phone" hint after ~5 s.
connect() probes the relay before showing the QR. If unreachable it throws RELAY_UNREACHABLE. During a pending request, three consecutive unreachable polls also reject with RELAY_UNREACHABLE.
A zero-dependency dev relay is in scripts/dev-relay.mjs. Run it, serve the dApp on your LAN IP, scan the QR.
Reader API
wallet.client (or new AseVaultReader()) handles reads with endpoint failover and timeouts:
| Method | Returns |
|---|---|
view(contract, method, args) | The unwrapped returnValue. JSON strings parsed, scalars as strings |
getBalance(address) | bigint (wei) |
balanceOf(contract, address) | via view |
getBlockNumber() | number |
getBlock(n) | { receipts: [{ txHash, success, error, recipient, events }] } |
getReceipt(hash) | receipt or null (pending/not found) |
getTxState(hash) | `'pending' |
waitForTx(hash, timeoutMs?) | `{ status: 'confirmed' |
waitReceipt(hash, timeoutMs?) | receipt or throws on timeout |
getContractEvents(contract, { event?, fromBlock?, toBlock?, limit? }) | matched events, newest first |
estimateGas(contract, method, args) | bigint gas units |
chainId() | decimal string |
useRpc(urls) / validateRpc(url) | endpoint pool + health probe |
The /view RPC returns { ok, returnValue, gasUsed }. view() resolves to returnValue directly, not the envelope.
Auto-detection
Inside the AseVault browser the wallet injects window.__asevaultBridge plus window.asevault/window.asentum aliases (request(), connect(), disconnect(), accounts(), getAddress(), sendCall, sendTransfer, signMessage, ase_deploy). InAppTransport binds to __asevaultBridge, so installAseVaultProvider() can replace window.asevault without breaking the channel. The wallet pushes accountsChanged/disconnect/connect events into the page.
Protocol (v1)
asevault://v1/connect?origin=&name=&permissions=&callback=&nonce=->callback?asevault=1&session=<b64>(session carries the ML-DSA-65 proof overASEVAULT_CONNECT|<nonce>|<address>)asevault://v1/sign?session=&payload=<b64url>&callback=&nonce=&kind=->callback?asevault=1&tx=<hash>or?error=<code>&message=asevault://v1/disconnect?session=- Relay:
POST/GET {relay}/r/<nonce>results,POST/GET {relay}/qqueued requests
Security
- Keys never leave AseVault. dApps see only address, public key, signatures and broadcast results.
- Every request requires explicit in-app approval.
- Sessions are origin-scoped and revocable inside AseVault (Connected Apps).
Full integration guide: CONNECT.md.
A product of Asepad.fun.